Compare commits

...

16 Commits

Author SHA1 Message Date
github-actions[bot]
6eba5be864 Bump version to 2.9.3 (#16510)
Signed-off-by: GitHub <noreply@github.com>
Co-authored-by: crenshaw-dev <crenshaw-dev@users.noreply.github.com>
2023-12-01 18:05:06 -05:00
gcp-cherry-pick-bot[bot]
cc56c9e8a2 fix(repo-server): excess git requests, resolveReferencedSources and runManifestGenAsync not using cache (Issue #14725) (#16410) (#16494)
* fix(repo-server): excess git requests part 1, resolveReferencedSources and runManifestGenAsync



* fix: remove unnecessary settings instantiation



---------

Signed-off-by: nromriell <nateromriell@gmail.com>
Co-authored-by: Nathan Romriell <nateromriell@gmail.com>
2023-11-30 10:20:03 -05:00
gcp-cherry-pick-bot[bot]
30f68e1041 fix(controller): Address diff cache miss issues (#16458) (#16485)
* fix: Address diff cache miss issues



* validate mergo.Merge errors



* Address review comments



* Allow setting log level at the controller



* remove unnecessary log setup



---------

Signed-off-by: Leonardo Luz Almeida <leonardo_almeida@intuit.com>
Co-authored-by: Leonardo Luz Almeida <leoluz@users.noreply.github.com>
2023-11-29 11:09:11 -05:00
Alexander Matyushentsev
e63273e4c1 fix: cherry-pick fixed cli admin dashboard cmd (#16457)
* chore(cli): clarify core mode code (#15800)

* chore(cli): clarify core mode code

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* rename function

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

---------

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* fix: fixed cli admin dashboard cmd (#16430)

* fix: fixed cli admin dashboard cmd

Signed-off-by: Soumya Ghosh Dastidar <gdsoumya@gmail.com>

* feat: update docs

Signed-off-by: Soumya Ghosh Dastidar <gdsoumya@gmail.com>

---------

Signed-off-by: Soumya Ghosh Dastidar <gdsoumya@gmail.com>

* chore(ui): Change testEnvironment from node to jsdom (#16287)

Signed-off-by: Rafal Pelczar <rafal@akuity.io>

---------

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Signed-off-by: Soumya Ghosh Dastidar <gdsoumya@gmail.com>
Signed-off-by: Rafal Pelczar <rafal@akuity.io>
Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Co-authored-by: Soumya Ghosh Dastidar <44349253+gdsoumya@users.noreply.github.com>
Co-authored-by: Rafal <rafal@akuity.io>
2023-11-27 14:16:31 -08:00
gcp-cherry-pick-bot[bot]
d5eaaa3527 fix(ui): Issues with overlapping content in the app details view on smaller screens (#16268) (#16407)
Signed-off-by: Rafal Pelczar <rafal@akuity.io>
Co-authored-by: Rafal <rafal@akuity.io>
2023-11-20 16:38:47 -05:00
github-actions[bot]
c5ea5c4df5 Bump version to 2.9.2 (#16405)
Signed-off-by: GitHub <noreply@github.com>
Co-authored-by: gdsoumya <gdsoumya@users.noreply.github.com>
2023-11-20 22:45:24 +05:30
gdsoumya
c2c9746050 feat: add support for ALL_PROXY (#10451) (#16401)
Signed-off-by: xniu <cnxuniu@gmail.com>
Co-authored-by: yushiwho <cnxuniu@gmail.com>
Co-authored-by: xniu <xniu@nvidia.com>
2023-11-20 10:32:39 -05:00
gcp-cherry-pick-bot[bot]
78cd50b2c7 fix: set max for max cookie number to math.MaxInt (#16388) (#16397) 2023-11-20 10:04:04 -05:00
gcp-cherry-pick-bot[bot]
dd86b08369 docs: fix documentation of ignoreApplicationDifferences (#16365) (#16380)
Signed-off-by: Talia Stocks <928827+taliastocks@users.noreply.github.com>
Co-authored-by: Talia Stocks <928827+taliastocks@users.noreply.github.com>
2023-11-17 15:25:08 -05:00
Michael Over
0ca43663c9 bump helm 3.13.2 (#16344)
Signed-off-by: Michael Over <michael.over@mimacom.com>
Co-authored-by: Michael Over <michael.over@mimacom.com>
2023-11-15 11:36:13 -05:00
Blake Pettersson
d4c37e2521 fix: check for double definition (#16335)
A simpler fix - don't add a managed namespace to the targetObjs list if
a namespace already exists in the application source.

Signed-off-by: Blake Pettersson <blake.pettersson@gmail.com>
2023-11-14 15:17:20 -05:00
Michael Crenshaw
ba60fadd94 docs: fix upgrade instructions (#16327)
Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2023-11-14 13:38:50 -05:00
github-actions[bot]
58b04e5e11 Bump version to 2.9.1 (#16325)
Signed-off-by: GitHub <noreply@github.com>
Co-authored-by: crenshaw-dev <crenshaw-dev@users.noreply.github.com>
2023-11-14 10:07:30 -05:00
Ishita Sequeira
3acd5ee30d fix(appset): ignoreApplicationDifferences not working (#15965) (#16299)
* fix(appset): ignoreApplicationDifferences not working



* tests, docs



* link to enhancement request



* handle error



* Update docs/operator-manual/applicationset/Controlling-Resource-Modification.md




* fix bug, fix docs



* fix docs



* normalize empty syncPolicy field



---------

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Co-authored-by: Hugues Peccatte <hugues.peccatte@gmail.com>
2023-11-13 15:21:36 -05:00
gcp-cherry-pick-bot[bot]
45d5de702e fix: check for double definition (#15670) (#16271)
* fix: check for double definition

As found in #13965 (and as a follow-up to #13999), we also need to
define what happens if _both_ managedNamespaceMetadata _and_ an
Application manifest are both defined for the same namespace.

The idea here is that if that happens, we emit an
`ApplicationConditionRepeatedResourceWarning`, and set the sync status
to `Unknown`, since it's unclear what is supposed to happen.

The user will then have the option of removing one of the two
definitions.



* fix: check for double definition

A simpler fix - don't add a managed namespace to the targetObjs list if
a namespace already exists in the application source.



* test: add test cases

This adds a test case showing that an ns manifest will override
`managedNamespaceMetadata` without failing horribly (as it currently
does on `HEAD`), as well as a "standard" mNMd diff vs live.



---------

Signed-off-by: Blake Pettersson <blake.pettersson@gmail.com>
Co-authored-by: Blake Pettersson <blake.pettersson@gmail.com>
2023-11-09 17:30:21 -05:00
gcp-cherry-pick-bot[bot]
b8efc8b1ab feat(appset): fromYaml, fromYamlArray toYaml functions (#15063) (#16289)
* feat(appset): fromYaml, fromYamlArray toYaml functions



* fix(11993): Document and error messages



---------

Signed-off-by: Geoffrey Muselli <geoffrey.muselli@gmail.com>
Signed-off-by: gmuselli <geoffrey.muselli@gmail.com>
Co-authored-by: Geoffrey MUSELLI <geoffrey.muselli@gmail.com>
2023-11-09 00:30:15 -05:00
48 changed files with 2119 additions and 648 deletions

View File

@@ -1 +1 @@
2.9.0
2.9.3

View File

@@ -16,7 +16,6 @@ package controllers
import (
"context"
"encoding/json"
"fmt"
"reflect"
"time"
@@ -25,7 +24,6 @@ import (
corev1 "k8s.io/api/core/v1"
apierr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
@@ -46,7 +44,6 @@ import (
"github.com/argoproj/argo-cd/v2/applicationset/generators"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
"github.com/argoproj/argo-cd/v2/common"
argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
"github.com/argoproj/argo-cd/v2/util/db"
"github.com/argoproj/argo-cd/v2/util/glob"
@@ -626,7 +623,7 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
},
}
action, err := utils.CreateOrUpdate(ctx, r.Client, found, func() error {
action, err := utils.CreateOrUpdate(ctx, appLog, r.Client, applicationSet.Spec.IgnoreApplicationDifferences, found, func() error {
// Copy only the Application/ObjectMeta fields that are significant, from the generatedApp
found.Spec = generatedApp.Spec
@@ -679,13 +676,6 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
found.ObjectMeta.Finalizers = generatedApp.Finalizers
found.ObjectMeta.Labels = generatedApp.Labels
if found != nil && len(found.Spec.IgnoreDifferences) > 0 {
err := applyIgnoreDifferences(applicationSet.Spec.IgnoreApplicationDifferences, found, generatedApp)
if err != nil {
return fmt.Errorf("failed to apply ignore differences: %w", err)
}
}
return controllerutil.SetControllerReference(&applicationSet, found, r.Scheme)
})
@@ -703,54 +693,6 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
return firstError
}
// applyIgnoreDifferences applies the ignore differences rules to the found application. It modifies the found application in place.
func applyIgnoreDifferences(applicationSetIgnoreDifferences argov1alpha1.ApplicationSetIgnoreDifferences, found *argov1alpha1.Application, generatedApp argov1alpha1.Application) error {
diffConfig, err := argodiff.NewDiffConfigBuilder().
WithDiffSettings(applicationSetIgnoreDifferences.ToApplicationIgnoreDifferences(), nil, false).
WithNoCache().
Build()
if err != nil {
return fmt.Errorf("failed to build diff config: %w", err)
}
unstructuredFound, err := appToUnstructured(found)
if err != nil {
return fmt.Errorf("failed to convert found application to unstructured: %w", err)
}
unstructuredGenerated, err := appToUnstructured(&generatedApp)
if err != nil {
return fmt.Errorf("failed to convert found application to unstructured: %w", err)
}
result, err := argodiff.Normalize([]*unstructured.Unstructured{unstructuredFound}, []*unstructured.Unstructured{unstructuredGenerated}, diffConfig)
if err != nil {
return fmt.Errorf("failed to normalize application spec: %w", err)
}
if len(result.Targets) != 1 {
return fmt.Errorf("expected 1 normalized application, got %d", len(result.Targets))
}
jsonNormalized, err := json.Marshal(result.Targets[0].Object)
if err != nil {
return fmt.Errorf("failed to marshal normalized app to json: %w", err)
}
err = json.Unmarshal(jsonNormalized, &found)
if err != nil {
return fmt.Errorf("failed to unmarshal normalized app json to structured app: %w", err)
}
// Prohibit jq queries from mutating silly things.
found.TypeMeta = generatedApp.TypeMeta
found.Name = generatedApp.Name
found.Namespace = generatedApp.Namespace
found.Operation = generatedApp.Operation
return nil
}
func appToUnstructured(app *argov1alpha1.Application) (*unstructured.Unstructured, error) {
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(app)
if err != nil {
return nil, fmt.Errorf("failed to convert app object to unstructured: %w", err)
}
return &unstructured.Unstructured{Object: u}, nil
}
// createInCluster will filter from the desiredApplications only the application that needs to be created
// Then it will call createOrUpdateInCluster to do the actual create
func (r *ApplicationSetReconciler) createInCluster(ctx context.Context, applicationSet argov1alpha1.ApplicationSet, desiredApplications []argov1alpha1.Application) error {
@@ -904,7 +846,11 @@ func (r *ApplicationSetReconciler) removeFinalizerOnInvalidDestination(ctx conte
if len(newFinalizers) != len(app.Finalizers) {
updated := app.DeepCopy()
updated.Finalizers = newFinalizers
if err := r.Client.Patch(ctx, updated, client.MergeFrom(app)); err != nil {
patch := client.MergeFrom(app)
if log.IsLevelEnabled(log.DebugLevel) {
utils.LogPatch(appLog, patch, updated)
}
if err := r.Client.Patch(ctx, updated, patch); err != nil {
return fmt.Errorf("error updating finalizers: %w", err)
}
r.updateCache(ctx, updated, appLog)

View File

@@ -12,8 +12,6 @@ import (
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -981,6 +979,296 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
},
},
},
}, {
// For this use case: https://github.com/argoproj/argo-cd/issues/9101#issuecomment-1191138278
name: "Ensure that ignored targetRevision difference doesn't cause an update, even if another field changes",
appSet: v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: v1alpha1.ApplicationSetSpec{
IgnoreApplicationDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
{JQPathExpressions: []string{".spec.source.targetRevision"}},
},
Template: v1alpha1.ApplicationSetTemplate{
Spec: v1alpha1.ApplicationSpec{
Project: "project",
Source: &v1alpha1.ApplicationSource{
RepoURL: "https://git.example.com/test-org/test-repo.git",
TargetRevision: "foo",
},
},
},
},
},
existingApps: []v1alpha1.Application{
{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "app1",
Namespace: "namespace",
ResourceVersion: "2",
},
Spec: v1alpha1.ApplicationSpec{
Project: "project",
Source: &v1alpha1.ApplicationSource{
RepoURL: "https://git.example.com/test-org/test-repo.git",
TargetRevision: "bar",
},
},
},
},
desiredApps: []v1alpha1.Application{
{
ObjectMeta: metav1.ObjectMeta{
Name: "app1",
},
Spec: v1alpha1.ApplicationSpec{
Project: "project",
Source: &v1alpha1.ApplicationSource{
RepoURL: "https://git.example.com/test-org/test-repo.git",
// The targetRevision is ignored, so this should not be updated.
TargetRevision: "foo",
// This should be updated.
Helm: &v1alpha1.ApplicationSourceHelm{
Parameters: []v1alpha1.HelmParameter{
{Name: "hi", Value: "there"},
},
},
},
},
},
},
expected: []v1alpha1.Application{
{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "app1",
Namespace: "namespace",
ResourceVersion: "3",
},
Spec: v1alpha1.ApplicationSpec{
Project: "project",
Source: &v1alpha1.ApplicationSource{
RepoURL: "https://git.example.com/test-org/test-repo.git",
// This is the existing value from the cluster, which should not be updated because the field is ignored.
TargetRevision: "bar",
// This was missing on the cluster, so it should be added.
Helm: &v1alpha1.ApplicationSourceHelm{
Parameters: []v1alpha1.HelmParameter{
{Name: "hi", Value: "there"},
},
},
},
},
},
},
}, {
// For this use case: https://github.com/argoproj/argo-cd/pull/14743#issuecomment-1761954799
name: "ignore parameters added to a multi-source app in the cluster",
appSet: v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: v1alpha1.ApplicationSetSpec{
IgnoreApplicationDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
{JQPathExpressions: []string{`.spec.sources[] | select(.repoURL | contains("test-repo")).helm.parameters`}},
},
Template: v1alpha1.ApplicationSetTemplate{
Spec: v1alpha1.ApplicationSpec{
Project: "project",
Sources: []v1alpha1.ApplicationSource{
{
RepoURL: "https://git.example.com/test-org/test-repo.git",
Helm: &v1alpha1.ApplicationSourceHelm{
Values: "foo: bar",
},
},
},
},
},
},
},
existingApps: []v1alpha1.Application{
{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "app1",
Namespace: "namespace",
ResourceVersion: "2",
},
Spec: v1alpha1.ApplicationSpec{
Project: "project",
Sources: []v1alpha1.ApplicationSource{
{
RepoURL: "https://git.example.com/test-org/test-repo.git",
Helm: &v1alpha1.ApplicationSourceHelm{
Values: "foo: bar",
Parameters: []v1alpha1.HelmParameter{
{Name: "hi", Value: "there"},
},
},
},
},
},
},
},
desiredApps: []v1alpha1.Application{
{
ObjectMeta: metav1.ObjectMeta{
Name: "app1",
},
Spec: v1alpha1.ApplicationSpec{
Project: "project",
Sources: []v1alpha1.ApplicationSource{
{
RepoURL: "https://git.example.com/test-org/test-repo.git",
Helm: &v1alpha1.ApplicationSourceHelm{
Values: "foo: bar",
},
},
},
},
},
},
expected: []v1alpha1.Application{
{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "app1",
Namespace: "namespace",
// This should not be updated, because reconciliation shouldn't modify the App.
ResourceVersion: "2",
},
Spec: v1alpha1.ApplicationSpec{
Project: "project",
Sources: []v1alpha1.ApplicationSource{
{
RepoURL: "https://git.example.com/test-org/test-repo.git",
Helm: &v1alpha1.ApplicationSourceHelm{
Values: "foo: bar",
Parameters: []v1alpha1.HelmParameter{
// This existed only in the cluster, but it shouldn't be removed, because the field is ignored.
{Name: "hi", Value: "there"},
},
},
},
},
},
},
},
}, {
name: "Demonstrate limitation of MergePatch", // Maybe we can fix this in Argo CD 3.0: https://github.com/argoproj/argo-cd/issues/15975
appSet: v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: v1alpha1.ApplicationSetSpec{
IgnoreApplicationDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
{JQPathExpressions: []string{`.spec.sources[] | select(.repoURL | contains("test-repo")).helm.parameters`}},
},
Template: v1alpha1.ApplicationSetTemplate{
Spec: v1alpha1.ApplicationSpec{
Project: "project",
Sources: []v1alpha1.ApplicationSource{
{
RepoURL: "https://git.example.com/test-org/test-repo.git",
Helm: &v1alpha1.ApplicationSourceHelm{
Values: "new: values",
},
},
},
},
},
},
},
existingApps: []v1alpha1.Application{
{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "app1",
Namespace: "namespace",
ResourceVersion: "2",
},
Spec: v1alpha1.ApplicationSpec{
Project: "project",
Sources: []v1alpha1.ApplicationSource{
{
RepoURL: "https://git.example.com/test-org/test-repo.git",
Helm: &v1alpha1.ApplicationSourceHelm{
Values: "foo: bar",
Parameters: []v1alpha1.HelmParameter{
{Name: "hi", Value: "there"},
},
},
},
},
},
},
},
desiredApps: []v1alpha1.Application{
{
ObjectMeta: metav1.ObjectMeta{
Name: "app1",
},
Spec: v1alpha1.ApplicationSpec{
Project: "project",
Sources: []v1alpha1.ApplicationSource{
{
RepoURL: "https://git.example.com/test-org/test-repo.git",
Helm: &v1alpha1.ApplicationSourceHelm{
Values: "new: values",
},
},
},
},
},
},
expected: []v1alpha1.Application{
{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "app1",
Namespace: "namespace",
ResourceVersion: "3",
},
Spec: v1alpha1.ApplicationSpec{
Project: "project",
Sources: []v1alpha1.ApplicationSource{
{
RepoURL: "https://git.example.com/test-org/test-repo.git",
Helm: &v1alpha1.ApplicationSourceHelm{
Values: "new: values",
// The Parameters field got blown away, because the values field changed. MergePatch
// doesn't merge list items, it replaces the whole list if an item changes.
// If we eventually add a `name` field to Sources, we can use StrategicMergePatch.
},
},
},
},
},
},
},
} {
@@ -1014,7 +1302,6 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
}, got)
err = controllerutil.SetControllerReference(&c.appSet, &obj, r.Scheme)
assert.Nil(t, err)
assert.Equal(t, obj, *got)
}
})
@@ -5719,173 +6006,3 @@ func TestOwnsHandler(t *testing.T) {
})
}
}
func Test_applyIgnoreDifferences(t *testing.T) {
appMeta := metav1.TypeMeta{
APIVersion: v1alpha1.ApplicationSchemaGroupVersionKind.GroupVersion().String(),
Kind: v1alpha1.ApplicationSchemaGroupVersionKind.Kind,
}
testCases := []struct {
name string
ignoreDifferences v1alpha1.ApplicationSetIgnoreDifferences
foundApp string
generatedApp string
expectedApp string
}{
{
name: "empty ignoreDifferences",
foundApp: `
spec: {}`,
generatedApp: `
spec: {}`,
expectedApp: `
spec: {}`,
},
{
// For this use case: https://github.com/argoproj/argo-cd/issues/9101#issuecomment-1191138278
name: "ignore target revision with jq",
ignoreDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
{JQPathExpressions: []string{".spec.source.targetRevision"}},
},
foundApp: `
spec:
source:
targetRevision: foo`,
generatedApp: `
spec:
source:
targetRevision: bar`,
expectedApp: `
spec:
source:
targetRevision: foo`,
},
{
// For this use case: https://github.com/argoproj/argo-cd/issues/9101#issuecomment-1103593714
name: "ignore helm parameter with jq",
ignoreDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
{JQPathExpressions: []string{`.spec.source.helm.parameters | select(.name == "image.tag")`}},
},
foundApp: `
spec:
source:
helm:
parameters:
- name: image.tag
value: test
- name: another
value: value`,
generatedApp: `
spec:
source:
helm:
parameters:
- name: image.tag
value: v1.0.0
- name: another
value: value`,
expectedApp: `
spec:
source:
helm:
parameters:
- name: image.tag
value: test
- name: another
value: value`,
},
{
// For this use case: https://github.com/argoproj/argo-cd/issues/9101#issuecomment-1191138278
name: "ignore auto-sync with jq",
ignoreDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
{JQPathExpressions: []string{".spec.syncPolicy.automated"}},
},
foundApp: `
spec:
syncPolicy:
retry:
limit: 5`,
generatedApp: `
spec:
syncPolicy:
automated:
selfHeal: true
retry:
limit: 5`,
expectedApp: `
spec:
syncPolicy:
retry:
limit: 5`,
},
{
// For this use case: https://github.com/argoproj/argo-cd/issues/9101#issuecomment-1420656537
name: "ignore a one-off annotation with jq",
ignoreDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
{JQPathExpressions: []string{`.metadata.annotations | select(.["foo.bar"] == "baz")`}},
},
foundApp: `
metadata:
annotations:
foo.bar: baz
some.other: annotation`,
generatedApp: `
metadata:
annotations:
some.other: annotation`,
expectedApp: `
metadata:
annotations:
foo.bar: baz
some.other: annotation`,
},
{
// For this use case: https://github.com/argoproj/argo-cd/issues/9101#issuecomment-1515672638
name: "ignore the source.plugin field with a json pointer",
ignoreDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
{JSONPointers: []string{"/spec/source/plugin"}},
},
foundApp: `
spec:
source:
plugin:
parameters:
- name: url
string: https://example.com`,
generatedApp: `
spec:
source:
plugin:
parameters:
- name: url
string: https://example.com/wrong`,
expectedApp: `
spec:
source:
plugin:
parameters:
- name: url
string: https://example.com`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
foundApp := v1alpha1.Application{TypeMeta: appMeta}
err := yaml.Unmarshal([]byte(tc.foundApp), &foundApp)
require.NoError(t, err, tc.foundApp)
generatedApp := v1alpha1.Application{TypeMeta: appMeta}
err = yaml.Unmarshal([]byte(tc.generatedApp), &generatedApp)
require.NoError(t, err, tc.generatedApp)
err = applyIgnoreDifferences(tc.ignoreDifferences, &foundApp, generatedApp)
require.NoError(t, err)
jsonFound, err := json.Marshal(tc.foundApp)
require.NoError(t, err)
jsonExpected, err := json.Marshal(tc.expectedApp)
require.NoError(t, err)
assert.Equal(t, string(jsonExpected), string(jsonFound))
})
}
}

View File

@@ -2,19 +2,24 @@ package utils
import (
"context"
"encoding/json"
"fmt"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/util/argo"
argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
)
// CreateOrUpdate overrides "sigs.k8s.io/controller-runtime" function
@@ -30,7 +35,7 @@ import (
// The MutateFn is called regardless of creating or updating an object.
//
// It returns the executed operation and an error.
func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object, f controllerutil.MutateFn) (controllerutil.OperationResult, error) {
func CreateOrUpdate(ctx context.Context, logCtx *log.Entry, c client.Client, ignoreAppDifferences argov1alpha1.ApplicationSetIgnoreDifferences, obj *argov1alpha1.Application, f controllerutil.MutateFn) (controllerutil.OperationResult, error) {
key := client.ObjectKeyFromObject(obj)
if err := c.Get(ctx, key, obj); err != nil {
@@ -46,15 +51,24 @@ func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object, f c
return controllerutil.OperationResultCreated, nil
}
existingObj := obj.DeepCopyObject()
existing, ok := existingObj.(client.Object)
if !ok {
panic(fmt.Errorf("existing object is not a client.Object"))
}
normalizedLive := obj.DeepCopy()
// Mutate the live object to match the desired state.
if err := mutate(f, key, obj); err != nil {
return controllerutil.OperationResultNone, err
}
// Apply ignoreApplicationDifferences rules to remove ignored fields from both the live and the desired state. This
// prevents those differences from appearing in the diff and therefore in the patch.
err := applyIgnoreDifferences(ignoreAppDifferences, normalizedLive, obj)
if err != nil {
return controllerutil.OperationResultNone, fmt.Errorf("failed to apply ignore differences: %w", err)
}
// Normalize to avoid diffing on unimportant differences.
normalizedLive.Spec = *argo.NormalizeApplicationSpec(&normalizedLive.Spec)
obj.Spec = *argo.NormalizeApplicationSpec(&obj.Spec)
equality := conversion.EqualitiesOrDie(
func(a, b resource.Quantity) bool {
// Ignore formatting, only care that numeric value stayed the same.
@@ -79,23 +93,35 @@ func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object, f c
return a.Namespace == b.Namespace && a.Name == b.Name && a.Server == b.Server
},
)
// make sure updated object has the same apiVersion & kind as original object
if objKind, ok := obj.(schema.ObjectKind); ok {
if existingKind, ok := existing.(schema.ObjectKind); ok {
existingKind.SetGroupVersionKind(objKind.GroupVersionKind())
}
}
if equality.DeepEqual(existing, obj) {
if equality.DeepEqual(normalizedLive, obj) {
return controllerutil.OperationResultNone, nil
}
if err := c.Patch(ctx, obj, client.MergeFrom(existing)); err != nil {
patch := client.MergeFrom(normalizedLive)
if log.IsLevelEnabled(log.DebugLevel) {
LogPatch(logCtx, patch, obj)
}
if err := c.Patch(ctx, obj, patch); err != nil {
return controllerutil.OperationResultNone, err
}
return controllerutil.OperationResultUpdated, nil
}
func LogPatch(logCtx *log.Entry, patch client.Patch, obj *argov1alpha1.Application) {
patchBytes, err := patch.Data(obj)
if err != nil {
logCtx.Errorf("failed to generate patch: %v", err)
}
// Get the patch as a plain object so it is easier to work with in json logs.
var patchObj map[string]interface{}
err = json.Unmarshal(patchBytes, &patchObj)
if err != nil {
logCtx.Errorf("failed to unmarshal patch: %v", err)
}
logCtx.WithField("patch", patchObj).Debug("patching application")
}
// mutate wraps a MutateFn and applies validation to its result
func mutate(f controllerutil.MutateFn, key client.ObjectKey, obj client.Object) error {
if err := f(); err != nil {
@@ -106,3 +132,71 @@ func mutate(f controllerutil.MutateFn, key client.ObjectKey, obj client.Object)
}
return nil
}
// applyIgnoreDifferences applies the ignore differences rules to the found application. It modifies the applications in place.
func applyIgnoreDifferences(applicationSetIgnoreDifferences argov1alpha1.ApplicationSetIgnoreDifferences, found *argov1alpha1.Application, generatedApp *argov1alpha1.Application) error {
if len(applicationSetIgnoreDifferences) == 0 {
return nil
}
generatedAppCopy := generatedApp.DeepCopy()
diffConfig, err := argodiff.NewDiffConfigBuilder().
WithDiffSettings(applicationSetIgnoreDifferences.ToApplicationIgnoreDifferences(), nil, false).
WithNoCache().
Build()
if err != nil {
return fmt.Errorf("failed to build diff config: %w", err)
}
unstructuredFound, err := appToUnstructured(found)
if err != nil {
return fmt.Errorf("failed to convert found application to unstructured: %w", err)
}
unstructuredGenerated, err := appToUnstructured(generatedApp)
if err != nil {
return fmt.Errorf("failed to convert found application to unstructured: %w", err)
}
result, err := argodiff.Normalize([]*unstructured.Unstructured{unstructuredFound}, []*unstructured.Unstructured{unstructuredGenerated}, diffConfig)
if err != nil {
return fmt.Errorf("failed to normalize application spec: %w", err)
}
if len(result.Lives) != 1 {
return fmt.Errorf("expected 1 normalized application, got %d", len(result.Lives))
}
foundJsonNormalized, err := json.Marshal(result.Lives[0].Object)
if err != nil {
return fmt.Errorf("failed to marshal normalized app to json: %w", err)
}
foundNormalized := &argov1alpha1.Application{}
err = json.Unmarshal(foundJsonNormalized, &foundNormalized)
if err != nil {
return fmt.Errorf("failed to unmarshal normalized app to json: %w", err)
}
if len(result.Targets) != 1 {
return fmt.Errorf("expected 1 normalized application, got %d", len(result.Targets))
}
foundNormalized.DeepCopyInto(found)
generatedJsonNormalized, err := json.Marshal(result.Targets[0].Object)
if err != nil {
return fmt.Errorf("failed to marshal normalized app to json: %w", err)
}
generatedAppNormalized := &argov1alpha1.Application{}
err = json.Unmarshal(generatedJsonNormalized, &generatedAppNormalized)
if err != nil {
return fmt.Errorf("failed to unmarshal normalized app json to structured app: %w", err)
}
generatedAppNormalized.DeepCopyInto(generatedApp)
// Prohibit jq queries from mutating silly things.
generatedApp.TypeMeta = generatedAppCopy.TypeMeta
generatedApp.Name = generatedAppCopy.Name
generatedApp.Namespace = generatedAppCopy.Namespace
generatedApp.Operation = generatedAppCopy.Operation
return nil
}
func appToUnstructured(app client.Object) (*unstructured.Unstructured, error) {
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(app)
if err != nil {
return nil, fmt.Errorf("failed to convert app object to unstructured: %w", err)
}
return &unstructured.Unstructured{Object: u}, nil
}

View File

@@ -0,0 +1,234 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
func Test_applyIgnoreDifferences(t *testing.T) {
appMeta := metav1.TypeMeta{
APIVersion: v1alpha1.ApplicationSchemaGroupVersionKind.GroupVersion().String(),
Kind: v1alpha1.ApplicationSchemaGroupVersionKind.Kind,
}
testCases := []struct {
name string
ignoreDifferences v1alpha1.ApplicationSetIgnoreDifferences
foundApp string
generatedApp string
expectedApp string
}{
{
name: "empty ignoreDifferences",
foundApp: `
spec: {}`,
generatedApp: `
spec: {}`,
expectedApp: `
spec: {}`,
},
{
// For this use case: https://github.com/argoproj/argo-cd/issues/9101#issuecomment-1191138278
name: "ignore target revision with jq",
ignoreDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
{JQPathExpressions: []string{".spec.source.targetRevision"}},
},
foundApp: `
spec:
source:
targetRevision: foo`,
generatedApp: `
spec:
source:
targetRevision: bar`,
expectedApp: `
spec:
source:
targetRevision: foo`,
},
{
// For this use case: https://github.com/argoproj/argo-cd/issues/9101#issuecomment-1103593714
name: "ignore helm parameter with jq",
ignoreDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
{JQPathExpressions: []string{`.spec.source.helm.parameters | select(.name == "image.tag")`}},
},
foundApp: `
spec:
source:
helm:
parameters:
- name: image.tag
value: test
- name: another
value: value`,
generatedApp: `
spec:
source:
helm:
parameters:
- name: image.tag
value: v1.0.0
- name: another
value: value`,
expectedApp: `
spec:
source:
helm:
parameters:
- name: image.tag
value: test
- name: another
value: value`,
},
{
// For this use case: https://github.com/argoproj/argo-cd/issues/9101#issuecomment-1191138278
name: "ignore auto-sync in appset when it's not in the cluster with jq",
ignoreDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
{JQPathExpressions: []string{".spec.syncPolicy.automated"}},
},
foundApp: `
spec:
syncPolicy:
retry:
limit: 5`,
generatedApp: `
spec:
syncPolicy:
automated:
selfHeal: true
retry:
limit: 5`,
expectedApp: `
spec:
syncPolicy:
retry:
limit: 5`,
},
{
name: "ignore auto-sync in the cluster when it's not in the appset with jq",
ignoreDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
{JQPathExpressions: []string{".spec.syncPolicy.automated"}},
},
foundApp: `
spec:
syncPolicy:
automated:
selfHeal: true
retry:
limit: 5`,
generatedApp: `
spec:
syncPolicy:
retry:
limit: 5`,
expectedApp: `
spec:
syncPolicy:
automated:
selfHeal: true
retry:
limit: 5`,
},
{
// For this use case: https://github.com/argoproj/argo-cd/issues/9101#issuecomment-1420656537
name: "ignore a one-off annotation with jq",
ignoreDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
{JQPathExpressions: []string{`.metadata.annotations | select(.["foo.bar"] == "baz")`}},
},
foundApp: `
metadata:
annotations:
foo.bar: baz
some.other: annotation`,
generatedApp: `
metadata:
annotations:
some.other: annotation`,
expectedApp: `
metadata:
annotations:
foo.bar: baz
some.other: annotation`,
},
{
// For this use case: https://github.com/argoproj/argo-cd/issues/9101#issuecomment-1515672638
name: "ignore the source.plugin field with a json pointer",
ignoreDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
{JSONPointers: []string{"/spec/source/plugin"}},
},
foundApp: `
spec:
source:
plugin:
parameters:
- name: url
string: https://example.com`,
generatedApp: `
spec:
source:
plugin:
parameters:
- name: url
string: https://example.com/wrong`,
expectedApp: `
spec:
source:
plugin:
parameters:
- name: url
string: https://example.com`,
},
{
// For this use case: https://github.com/argoproj/argo-cd/pull/14743#issuecomment-1761954799
name: "ignore parameters added to a multi-source app in the cluster",
ignoreDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
{JQPathExpressions: []string{`.spec.sources[] | select(.repoURL | contains("test-repo")).helm.parameters`}},
},
foundApp: `
spec:
sources:
- repoURL: https://git.example.com/test-org/test-repo
helm:
parameters:
- name: test
value: hi`,
generatedApp: `
spec:
sources:
- repoURL: https://git.example.com/test-org/test-repo`,
expectedApp: `
spec:
sources:
- repoURL: https://git.example.com/test-org/test-repo
helm:
parameters:
- name: test
value: hi`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
foundApp := v1alpha1.Application{TypeMeta: appMeta}
err := yaml.Unmarshal([]byte(tc.foundApp), &foundApp)
require.NoError(t, err, tc.foundApp)
generatedApp := v1alpha1.Application{TypeMeta: appMeta}
err = yaml.Unmarshal([]byte(tc.generatedApp), &generatedApp)
require.NoError(t, err, tc.generatedApp)
err = applyIgnoreDifferences(tc.ignoreDifferences, &foundApp, &generatedApp)
require.NoError(t, err)
yamlFound, err := yaml.Marshal(tc.foundApp)
require.NoError(t, err)
yamlExpected, err := yaml.Marshal(tc.expectedApp)
require.NoError(t, err)
assert.Equal(t, string(yamlExpected), string(yamlFound))
})
}
}

View File

@@ -0,0 +1,71 @@
package utils
import (
"regexp"
"strings"
"sigs.k8s.io/yaml"
)
// SanitizeName sanitizes the name in accordance with the below rules
// 1. contain no more than 253 characters
// 2. contain only lowercase alphanumeric characters, '-' or '.'
// 3. start and end with an alphanumeric character
func SanitizeName(name string) string {
invalidDNSNameChars := regexp.MustCompile("[^-a-z0-9.]")
maxDNSNameLength := 253
name = strings.ToLower(name)
name = invalidDNSNameChars.ReplaceAllString(name, "-")
if len(name) > maxDNSNameLength {
name = name[:maxDNSNameLength]
}
return strings.Trim(name, "-.")
}
// This has been copied from helm and may be removed as soon as it is retrofited in sprig
// toYAML takes an interface, marshals it to yaml, and returns a string. It will
// always return a string, even on marshal error (empty string).
//
// This is designed to be called from a template.
func toYAML(v interface{}) (string, error) {
data, err := yaml.Marshal(v)
if err != nil {
// Swallow errors inside of a template.
return "", err
}
return strings.TrimSuffix(string(data), "\n"), nil
}
// This has been copied from helm and may be removed as soon as it is retrofited in sprig
// fromYAML converts a YAML document into a map[string]interface{}.
//
// This is not a general-purpose YAML parser, and will not parse all valid
// YAML documents. Additionally, because its intended use is within templates
// it tolerates errors. It will insert the returned error message string into
// m["Error"] in the returned map.
func fromYAML(str string) (map[string]interface{}, error) {
m := map[string]interface{}{}
if err := yaml.Unmarshal([]byte(str), &m); err != nil {
return nil, err
}
return m, nil
}
// This has been copied from helm and may be removed as soon as it is retrofited in sprig
// fromYAMLArray converts a YAML array into a []interface{}.
//
// This is not a general-purpose YAML parser, and will not parse all valid
// YAML documents. Additionally, because its intended use is within templates
// it tolerates errors. It will insert the returned error message string as
// the first and only item in the returned array.
func fromYAMLArray(str string) ([]interface{}, error) {
a := []interface{}{}
if err := yaml.Unmarshal([]byte(str), &a); err != nil {
return nil, err
}
return a, nil
}

View File

@@ -32,6 +32,9 @@ func init() {
delete(sprigFuncMap, "expandenv")
delete(sprigFuncMap, "getHostByName")
sprigFuncMap["normalize"] = SanitizeName
sprigFuncMap["toYaml"] = toYAML
sprigFuncMap["fromYaml"] = fromYAML
sprigFuncMap["fromYamlArray"] = fromYAMLArray
}
type Renderer interface {
@@ -431,23 +434,6 @@ func NormalizeBitbucketBasePath(basePath string) string {
return basePath
}
// SanitizeName sanitizes the name in accordance with the below rules
// 1. contain no more than 253 characters
// 2. contain only lowercase alphanumeric characters, '-' or '.'
// 3. start and end with an alphanumeric character
func SanitizeName(name string) string {
invalidDNSNameChars := regexp.MustCompile("[^-a-z0-9.]")
maxDNSNameLength := 253
name = strings.ToLower(name)
name = invalidDNSNameChars.ReplaceAllString(name, "-")
if len(name) > maxDNSNameLength {
name = name[:maxDNSNameLength]
}
return strings.Trim(name, "-.")
}
func getTlsConfigWithCACert(scmRootCAPath string) *tls.Config {
tlsConfig := &tls.Config{}

View File

@@ -555,6 +555,64 @@ func TestRenderTemplateParamsGoTemplate(t *testing.T) {
templateOptions: []string{"missingkey=error"},
errorMessage: `failed to execute go template --> {{.doesnotexist}} <--: template: :1:6: executing "" at <.doesnotexist>: map has no entry for key "doesnotexist"`,
},
{
name: "toYaml",
fieldVal: `{{ toYaml . | indent 2 }}`,
expectedVal: " foo:\n bar:\n bool: true\n number: 2\n str: Hello world",
params: map[string]interface{}{
"foo": map[string]interface{}{
"bar": map[string]interface{}{
"bool": true,
"number": 2,
"str": "Hello world",
},
},
},
},
{
name: "toYaml Error",
fieldVal: `{{ toYaml . | indent 2 }}`,
expectedVal: " foo:\n bar:\n bool: true\n number: 2\n str: Hello world",
errorMessage: "failed to execute go template {{ toYaml . | indent 2 }}: template: :1:3: executing \"\" at <toYaml .>: error calling toYaml: error marshaling into JSON: json: unsupported type: func(*string)",
params: map[string]interface{}{
"foo": func(test *string) {
},
},
},
{
name: "fromYaml",
fieldVal: `{{ get (fromYaml .value) "hello" }}`,
expectedVal: "world",
params: map[string]interface{}{
"value": "hello: world",
},
},
{
name: "fromYaml error",
fieldVal: `{{ get (fromYaml .value) "hello" }}`,
expectedVal: "world",
errorMessage: "failed to execute go template {{ get (fromYaml .value) \"hello\" }}: template: :1:8: executing \"\" at <fromYaml .value>: error calling fromYaml: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type map[string]interface {}",
params: map[string]interface{}{
"value": "non\n compliant\n yaml",
},
},
{
name: "fromYamlArray",
fieldVal: `{{ fromYamlArray .value | last }}`,
expectedVal: "bonjour tout le monde",
params: map[string]interface{}{
"value": "- hello world\n- bonjour tout le monde",
},
},
{
name: "fromYamlArray error",
fieldVal: `{{ fromYamlArray .value | last }}`,
expectedVal: "bonjour tout le monde",
errorMessage: "failed to execute go template {{ fromYamlArray .value | last }}: template: :1:3: executing \"\" at <fromYamlArray .value>: error calling fromYamlArray: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type []interface {}",
params: map[string]interface{}{
"value": "non\n compliant\n yaml",
},
},
}
for _, test := range tests {

View File

@@ -57,7 +57,7 @@ func NewAdminCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command.AddCommand(NewRepoCommand())
command.AddCommand(NewImportCommand())
command.AddCommand(NewExportCommand())
command.AddCommand(NewDashboardCommand())
command.AddCommand(NewDashboardCommand(clientOpts))
command.AddCommand(NewNotificationsCommand())
command.AddCommand(NewInitialPasswordCommand())

View File

@@ -3,7 +3,9 @@ package admin
import (
"fmt"
"github.com/argoproj/argo-cd/v2/util/cli"
"github.com/spf13/cobra"
"k8s.io/client-go/tools/clientcmd"
"github.com/argoproj/argo-cd/v2/cmd/argocd/commands/headless"
"github.com/argoproj/argo-cd/v2/cmd/argocd/commands/initialize"
@@ -14,11 +16,12 @@ import (
"github.com/argoproj/argo-cd/v2/util/errors"
)
func NewDashboardCommand() *cobra.Command {
func NewDashboardCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
port int
address string
compressionStr string
clientConfig clientcmd.ClientConfig
)
cmd := &cobra.Command{
Use: "dashboard",
@@ -28,12 +31,13 @@ func NewDashboardCommand() *cobra.Command {
compression, err := cache.CompressionTypeFromString(compressionStr)
errors.CheckError(err)
errors.CheckError(headless.StartLocalServer(ctx, &argocdclient.ClientOptions{Core: true}, initialize.RetrieveContextIfChanged(cmd.Flag("context")), &port, &address, compression))
clientOpts.Core = true
errors.CheckError(headless.MaybeStartLocalServer(ctx, clientOpts, initialize.RetrieveContextIfChanged(cmd.Flag("context")), &port, &address, compression, clientConfig))
println(fmt.Sprintf("Argo CD UI is available at http://%s:%d", address, port))
<-ctx.Done()
},
}
initialize.InitCommand(cmd)
clientConfig = cli.AddKubectlFlagsToSet(cmd.Flags())
cmd.Flags().IntVar(&port, "port", common.DefaultPortAPIServer, "Listen on given port")
cmd.Flags().StringVar(&address, "address", common.DefaultAddressAdminDashboard, "Listen on given address")
cmd.Flags().StringVar(&compressionStr, "redis-compress", env.StringFromEnv("REDIS_COMPRESSION", string(cache.RedisCompressionGZip)), "Enable this if the application controller is configured with redis compression enabled. (possible values: gzip, none)")

View File

@@ -148,13 +148,19 @@ func testAPI(ctx context.Context, clientOpts *apiclient.ClientOptions) error {
return nil
}
// StartLocalServer allows executing command in a headless mode: on the fly starts Argo CD API server and
// changes provided client options to use started API server port
func StartLocalServer(ctx context.Context, clientOpts *apiclient.ClientOptions, ctxStr string, port *int, address *string, compression cache.RedisCompressionType) error {
flags := pflag.NewFlagSet("tmp", pflag.ContinueOnError)
clientConfig := cli.AddKubectlFlagsToSet(flags)
// MaybeStartLocalServer allows executing command in a headless mode. If we're in core mode, starts the Argo CD API
// server on the fly and changes provided client options to use started API server port.
//
// If the clientOpts enables core mode, but the local config does not have core mode enabled, this function will
// not start the local server.
func MaybeStartLocalServer(ctx context.Context, clientOpts *apiclient.ClientOptions, ctxStr string, port *int, address *string, compression cache.RedisCompressionType, clientConfig clientcmd.ClientConfig) error {
if clientConfig == nil {
flags := pflag.NewFlagSet("tmp", pflag.ContinueOnError)
clientConfig = cli.AddKubectlFlagsToSet(flags)
}
startInProcessAPI := clientOpts.Core
if !startInProcessAPI {
// Core mode is enabled on client options. Check the local config to see if we should start the API server.
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
if err != nil {
return fmt.Errorf("error reading local config: %w", err)
@@ -164,9 +170,11 @@ func StartLocalServer(ctx context.Context, clientOpts *apiclient.ClientOptions,
if err != nil {
return fmt.Errorf("error resolving context: %w", err)
}
// There was a local config file, so determine whether core mode is enabled per the config file.
startInProcessAPI = configCtx.Server.Core
}
}
// If we're in core mode, start the API server on the fly.
if !startInProcessAPI {
return nil
}
@@ -238,6 +246,7 @@ func StartLocalServer(ctx context.Context, clientOpts *apiclient.ClientOptions,
if !cache2.WaitForCacheSync(ctx.Done(), srv.Initialized) {
log.Fatal("Timed out waiting for project cache to sync")
}
tries := 5
for i := 0; i < tries; i++ {
err = testAPI(ctx, clientOpts)
@@ -257,7 +266,9 @@ func NewClientOrDie(opts *apiclient.ClientOptions, c *cobra.Command) apiclient.C
ctx := c.Context()
ctxStr := initialize.RetrieveContextIfChanged(c.Flag("context"))
err := StartLocalServer(ctx, opts, ctxStr, nil, nil, cache.RedisCompressionNone)
// If we're in core mode, start the API server on the fly and configure the client `opts` to use it.
// If we're not in core mode, this function call will do nothing.
err := MaybeStartLocalServer(ctx, opts, ctxStr, nil, nil, cache.RedisCompressionNone, nil)
if err != nil {
log.Fatal(err)
}

View File

@@ -107,6 +107,10 @@ type appStateManager struct {
persistResourceHealth bool
}
// getRepoObjs will generate the manifests for the given application delegating the
// task to the repo-server. It returns the list of generated manifests as unstructured
// objects. It also returns the full response from all calls to the repo server as the
// second argument.
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, error) {
ts := stats.NewTimingStats()
@@ -391,6 +395,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
now := metav1.Now()
var manifestInfos []*apiclient.ManifestResponse
targetNsExists := false
if len(localManifests) == 0 {
// If the length of revisions is not same as the length of sources,
@@ -453,6 +458,13 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
LastTransitionTime: &now,
})
}
// If we reach this path, this means that a namespace has been both defined in Git, as well in the
// application's managedNamespaceMetadata. We want to ensure that this manifest is the one being used instead
// of what is present in managedNamespaceMetadata.
if isManagedNamespace(targetObj, app) {
targetNsExists = true
}
}
ts.AddCheckpoint("dedup_ms")
@@ -511,7 +523,10 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
// entry in source control. In order for the namespace not to risk being pruned, we'll need to generate a
// namespace which we can compare the live namespace with. For that, we'll do the same as is done in
// gitops-engine, the difference here being that we create a managed namespace which is only used for comparison.
if isManagedNamespace(liveObj, app) {
//
// targetNsExists == true implies that it already exists as a target, so no need to add the namespace to the
// targetObjs array.
if isManagedNamespace(liveObj, app) && !targetNsExists {
nsSpec := &v1.Namespace{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: kubeutil.NamespaceKind}, ObjectMeta: metav1.ObjectMeta{Name: liveObj.GetName()}}
managedNs, err := kubeutil.ToUnstructured(nsSpec)
@@ -547,21 +562,16 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
manifestRevisions = append(manifestRevisions, manifestInfo.Revision)
}
// restore comparison using cached diff result if previous comparison was performed for the same revision
revisionChanged := len(manifestInfos) != len(sources) || !reflect.DeepEqual(app.Status.Sync.Revisions, manifestRevisions)
specChanged := !reflect.DeepEqual(app.Status.Sync.ComparedTo, v1alpha1.ComparedTo{Source: app.Spec.GetSource(), Destination: app.Spec.Destination, Sources: sources, IgnoreDifferences: app.Spec.IgnoreDifferences})
_, refreshRequested := app.IsRefreshRequested()
noCache = noCache || refreshRequested || app.Status.Expired(m.statusRefreshTimeout) || specChanged || revisionChanged
useDiffCache := useDiffCache(noCache, manifestInfos, sources, app, manifestRevisions, m.statusRefreshTimeout, logCtx)
diffConfigBuilder := argodiff.NewDiffConfigBuilder().
WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles).
WithTracking(appLabelKey, string(trackingMethod))
if noCache {
diffConfigBuilder.WithNoCache()
if useDiffCache {
diffConfigBuilder.WithCache(m.cache, app.InstanceName(m.namespace))
} else {
diffConfigBuilder.WithCache(m.cache, app.GetName())
diffConfigBuilder.WithNoCache()
}
gvkParser, err := m.getGVKParser(app.Spec.Destination.Server)
@@ -768,6 +778,46 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
return &compRes
}
// useDiffCache will determine if the diff should be calculated based
// on the existing live state cache or not.
func useDiffCache(noCache bool, manifestInfos []*apiclient.ManifestResponse, sources []v1alpha1.ApplicationSource, app *v1alpha1.Application, manifestRevisions []string, statusRefreshTimeout time.Duration, log *log.Entry) bool {
if noCache {
log.WithField("useDiffCache", "false").Debug("noCache is true")
return false
}
_, refreshRequested := app.IsRefreshRequested()
if refreshRequested {
log.WithField("useDiffCache", "false").Debug("refreshRequested")
return false
}
if app.Status.Expired(statusRefreshTimeout) {
log.WithField("useDiffCache", "false").Debug("app.status.expired")
return false
}
if len(manifestInfos) != len(sources) {
log.WithField("useDiffCache", "false").Debug("manifestInfos len != sources len")
return false
}
revisionChanged := !reflect.DeepEqual(app.Status.GetRevisions(), manifestRevisions)
if revisionChanged {
log.WithField("useDiffCache", "false").Debug("revisionChanged")
return false
}
currentSpec := app.BuildComparedToStatus()
specChanged := !reflect.DeepEqual(app.Status.Sync.ComparedTo, currentSpec)
if specChanged {
log.WithField("useDiffCache", "false").Debug("specChanged")
return false
}
log.WithField("useDiffCache", "true").Debug("using diff cache")
return true
}
func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource, revisions []string, sources []v1alpha1.ApplicationSource, hasMultipleSources bool, startedAt metav1.Time) error {
var nextID int64
if len(app.Status.History) > 0 {

View File

@@ -10,6 +10,9 @@ import (
synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
. "github.com/argoproj/gitops-engine/pkg/utils/testing"
"github.com/imdario/mergo"
"github.com/sirupsen/logrus"
logrustest "github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
@@ -89,6 +92,122 @@ func TestCompareAppStateNamespaceMetadataDiffers(t *testing.T) {
assert.Len(t, app.Status.Conditions, 0)
}
// TestCompareAppStateNamespaceMetadataDiffers tests comparison when managed namespace metadata differs to live and manifest ns
func TestCompareAppStateNamespaceMetadataDiffersToManifest(t *testing.T) {
ns := NewNamespace()
ns.SetName(test.FakeDestNamespace)
ns.SetNamespace(test.FakeDestNamespace)
ns.SetAnnotations(map[string]string{"bar": "bat"})
app := newFakeApp()
app.Spec.SyncPolicy.ManagedNamespaceMetadata = &argoappv1.ManagedNamespaceMetadata{
Labels: map[string]string{
"foo": "bar",
},
Annotations: map[string]string{
"foo": "bar",
},
}
app.Status.OperationState = &argoappv1.OperationState{
SyncResult: &argoappv1.SyncOperationResult{},
}
liveNs := ns.DeepCopy()
liveNs.SetAnnotations(nil)
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{toJSON(t, liveNs)},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
},
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
kube.GetResourceKey(ns): ns,
},
}
ctrl := newFakeController(&data)
sources := make([]argoappv1.ApplicationSource, 0)
sources = append(sources, app.Spec.GetSource())
revisions := make([]string, 0)
revisions = append(revisions, "")
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 1)
assert.Len(t, compRes.managedResources, 1)
assert.NotNil(t, compRes.diffResultList)
assert.Len(t, compRes.diffResultList.Diffs, 1)
result := NewNamespace()
assert.NoError(t, json.Unmarshal(compRes.diffResultList.Diffs[0].PredictedLive, result))
labels := result.GetLabels()
delete(labels, "kubernetes.io/metadata.name")
assert.Equal(t, map[string]string{}, labels)
// Manifests override definitions in managedNamespaceMetadata
assert.Equal(t, map[string]string{"bar": "bat"}, result.GetAnnotations())
assert.Len(t, app.Status.Conditions, 0)
}
// TestCompareAppStateNamespaceMetadata tests comparison when managed namespace metadata differs to live
func TestCompareAppStateNamespaceMetadata(t *testing.T) {
ns := NewNamespace()
ns.SetName(test.FakeDestNamespace)
ns.SetNamespace(test.FakeDestNamespace)
ns.SetAnnotations(map[string]string{"bar": "bat"})
app := newFakeApp()
app.Spec.SyncPolicy.ManagedNamespaceMetadata = &argoappv1.ManagedNamespaceMetadata{
Labels: map[string]string{
"foo": "bar",
},
Annotations: map[string]string{
"foo": "bar",
},
}
app.Status.OperationState = &argoappv1.OperationState{
SyncResult: &argoappv1.SyncOperationResult{},
}
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
},
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
kube.GetResourceKey(ns): ns,
},
}
ctrl := newFakeController(&data)
sources := make([]argoappv1.ApplicationSource, 0)
sources = append(sources, app.Spec.GetSource())
revisions := make([]string, 0)
revisions = append(revisions, "")
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 1)
assert.Len(t, compRes.managedResources, 1)
assert.NotNil(t, compRes.diffResultList)
assert.Len(t, compRes.diffResultList.Diffs, 1)
result := NewNamespace()
assert.NoError(t, json.Unmarshal(compRes.diffResultList.Diffs[0].PredictedLive, result))
labels := result.GetLabels()
delete(labels, "kubernetes.io/metadata.name")
assert.Equal(t, map[string]string{"foo": "bar"}, labels)
assert.Equal(t, map[string]string{"argocd.argoproj.io/sync-options": "ServerSideApply=true", "bar": "bat", "foo": "bar"}, result.GetAnnotations())
assert.Len(t, app.Status.Conditions, 0)
}
// TestCompareAppStateNamespaceMetadataIsTheSame tests comparison when managed namespace metadata is the same
func TestCompareAppStateNamespaceMetadataIsTheSame(t *testing.T) {
app := newFakeApp()
@@ -1223,3 +1342,252 @@ func TestIsLiveResourceManaged(t *testing.T) {
assert.True(t, manager.isSelfReferencedObj(managedWrongAPIGroup, config, appName, common.AnnotationKeyAppInstance, argo.TrackingMethodAnnotation))
})
}
func TestUseDiffCache(t *testing.T) {
type fixture struct {
testName string
noCache bool
manifestInfos []*apiclient.ManifestResponse
sources []argoappv1.ApplicationSource
app *argoappv1.Application
manifestRevisions []string
statusRefreshTimeout time.Duration
expectedUseCache bool
}
manifestInfos := func(revision string) []*apiclient.ManifestResponse {
return []*apiclient.ManifestResponse{
{
Manifests: []string{
"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"httpbin\"},\"name\":\"httpbin-svc\",\"namespace\":\"httpbin\"},\"spec\":{\"ports\":[{\"name\":\"http-port\",\"port\":7777,\"targetPort\":80},{\"name\":\"test\",\"port\":333}],\"selector\":{\"app\":\"httpbin\"}}}",
"{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"httpbin\"},\"name\":\"httpbin-deployment\",\"namespace\":\"httpbin\"},\"spec\":{\"replicas\":2,\"selector\":{\"matchLabels\":{\"app\":\"httpbin\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"httpbin\"}},\"spec\":{\"containers\":[{\"image\":\"kennethreitz/httpbin\",\"imagePullPolicy\":\"Always\",\"name\":\"httpbin\",\"ports\":[{\"containerPort\":80}]}]}}}}",
},
Namespace: "",
Server: "",
Revision: revision,
SourceType: "Kustomize",
VerifyResult: "",
},
}
}
sources := func() []argoappv1.ApplicationSource {
return []argoappv1.ApplicationSource{
{
RepoURL: "https://some-repo.com",
Path: "argocd/httpbin",
TargetRevision: "HEAD",
},
}
}
app := func(namespace string, revision string, refresh bool, a *argoappv1.Application) *argoappv1.Application {
app := &argoappv1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "httpbin",
Namespace: namespace,
},
Spec: argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: "https://some-repo.com",
Path: "argocd/httpbin",
TargetRevision: "HEAD",
},
Destination: argoappv1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "httpbin",
},
Project: "default",
SyncPolicy: &argoappv1.SyncPolicy{
SyncOptions: []string{
"CreateNamespace=true",
"ServerSideApply=true",
},
},
},
Status: argoappv1.ApplicationStatus{
Resources: []argoappv1.ResourceStatus{},
Sync: argoappv1.SyncStatus{
Status: argoappv1.SyncStatusCodeSynced,
ComparedTo: argoappv1.ComparedTo{
Source: argoappv1.ApplicationSource{
RepoURL: "https://some-repo.com",
Path: "argocd/httpbin",
TargetRevision: "HEAD",
},
Destination: argoappv1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "httpbin",
},
},
Revision: revision,
Revisions: []string{},
},
ReconciledAt: &metav1.Time{
Time: time.Now().Add(-time.Hour),
},
},
}
if refresh {
annotations := make(map[string]string)
annotations[argoappv1.AnnotationKeyRefresh] = string(argoappv1.RefreshTypeNormal)
app.SetAnnotations(annotations)
}
if a != nil {
err := mergo.Merge(app, a, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue)
if err != nil {
t.Fatalf("error merging app: %s", err)
}
}
return app
}
cases := []fixture{
{
testName: "will use diff cache",
noCache: false,
manifestInfos: manifestInfos("rev1"),
sources: sources(),
app: app("httpbin", "rev1", false, nil),
manifestRevisions: []string{"rev1"},
statusRefreshTimeout: time.Hour * 24,
expectedUseCache: true,
},
{
testName: "will use diff cache for multisource",
noCache: false,
manifestInfos: manifestInfos("rev1"),
sources: sources(),
app: app("httpbin", "", false, &argoappv1.Application{
Spec: argoappv1.ApplicationSpec{
Source: nil,
Sources: argoappv1.ApplicationSources{
{
RepoURL: "multisource repo1",
},
{
RepoURL: "multisource repo2",
},
},
},
Status: argoappv1.ApplicationStatus{
Resources: []argoappv1.ResourceStatus{},
Sync: argoappv1.SyncStatus{
Status: argoappv1.SyncStatusCodeSynced,
ComparedTo: argoappv1.ComparedTo{
Source: argoappv1.ApplicationSource{},
Sources: argoappv1.ApplicationSources{
{
RepoURL: "multisource repo1",
},
{
RepoURL: "multisource repo2",
},
},
},
Revisions: []string{"rev1", "rev2"},
},
ReconciledAt: &metav1.Time{
Time: time.Now().Add(-time.Hour),
},
},
}),
manifestRevisions: []string{"rev1", "rev2"},
statusRefreshTimeout: time.Hour * 24,
expectedUseCache: true,
},
{
testName: "will return false if nocache is true",
noCache: true,
manifestInfos: manifestInfos("rev1"),
sources: sources(),
app: app("httpbin", "rev1", false, nil),
manifestRevisions: []string{"rev1"},
statusRefreshTimeout: time.Hour * 24,
expectedUseCache: false,
},
{
testName: "will return false if requested refresh",
noCache: false,
manifestInfos: manifestInfos("rev1"),
sources: sources(),
app: app("httpbin", "rev1", true, nil),
manifestRevisions: []string{"rev1"},
statusRefreshTimeout: time.Hour * 24,
expectedUseCache: false,
},
{
testName: "will return false if status expired",
noCache: false,
manifestInfos: manifestInfos("rev1"),
sources: sources(),
app: app("httpbin", "rev1", false, nil),
manifestRevisions: []string{"rev1"},
statusRefreshTimeout: time.Minute,
expectedUseCache: false,
},
{
testName: "will return false if there is a new revision",
noCache: false,
manifestInfos: manifestInfos("rev1"),
sources: sources(),
app: app("httpbin", "rev1", false, nil),
manifestRevisions: []string{"rev2"},
statusRefreshTimeout: time.Hour * 24,
expectedUseCache: false,
},
{
testName: "will return false if app spec repo changed",
noCache: false,
manifestInfos: manifestInfos("rev1"),
sources: sources(),
app: app("httpbin", "rev1", false, &argoappv1.Application{
Spec: argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: "new-repo",
},
},
}),
manifestRevisions: []string{"rev1"},
statusRefreshTimeout: time.Hour * 24,
expectedUseCache: false,
},
{
testName: "will return false if app spec IgnoreDifferences changed",
noCache: false,
manifestInfos: manifestInfos("rev1"),
sources: sources(),
app: app("httpbin", "rev1", false, &argoappv1.Application{
Spec: argoappv1.ApplicationSpec{
IgnoreDifferences: []argoappv1.ResourceIgnoreDifferences{
{
Group: "app/v1",
Kind: "application",
Name: "httpbin",
Namespace: "httpbin",
JQPathExpressions: []string{"."},
},
},
},
}),
manifestRevisions: []string{"rev1"},
statusRefreshTimeout: time.Hour * 24,
expectedUseCache: false,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.testName, func(t *testing.T) {
// Given
t.Parallel()
logger, _ := logrustest.NewNullLogger()
log := logrus.NewEntry(logger)
// When
useDiffCache := useDiffCache(tc.noCache, tc.manifestInfos, tc.sources, tc.app, tc.manifestRevisions, tc.statusRefreshTimeout, log)
// Then
assert.Equal(t, useDiffCache, tc.expectedUseCache)
})
}
}

View File

@@ -33,6 +33,6 @@ spec:
- jsonPointers:
- /spec/source/targetRevision
- name: some-app
jqExpressions:
jqPathExpressions:
- .spec.source.helm.values

View File

@@ -6,7 +6,7 @@ These settings allow you to exert control over when, and how, changes are made t
Here are some of the controller settings that may be modified to alter the ApplicationSet controller's resource-handling behaviour.
### Dry run: prevent ApplicationSet from creating, modifying, or deleting all Applications
## Dry run: prevent ApplicationSet from creating, modifying, or deleting all Applications
To prevent the ApplicationSet controller from creating, modifying, or deleting any `Application` resources, you may enable `dry-run` mode. This essentially switches the controller into a "read only" mode, where the controller Reconcile loop will run, but no resources will be modified.
@@ -14,7 +14,7 @@ To enable dry-run, add `--dryrun true` to the ApplicationSet Deployment's contai
See 'How to modify ApplicationSet container parameters' below for detailed steps on how to add this parameter to the controller.
### Managed Applications modification Policies
## Managed Applications modification Policies
The ApplicationSet controller supports a parameter `--policy`, which is specified on launch (within the controller Deployment container), and which restricts what types of modifications will be made to managed Argo CD `Application` resources.
@@ -41,7 +41,7 @@ If the controller parameter `--policy` is set, it takes precedence on the field
This does not prevent deletion of Applications if the ApplicationSet is deleted
#### Controller parameter
### Controller parameter
To allow the ApplicationSet controller to *create* `Application` resources, but prevent any further modification, such as deletion, or modification of Application fields, add this parameter in the ApplicationSet controller:
```
@@ -59,7 +59,7 @@ spec:
applicationsSync: create-only
```
### Policy - `create-update`: Prevent ApplicationSet controller from deleting Applications
## Policy - `create-update`: Prevent ApplicationSet controller from deleting Applications
To allow the ApplicationSet controller to create or modify `Application` resources, but prevent Applications from being deleted, add the following parameter to the ApplicationSet controller `Deployment`:
```
@@ -79,7 +79,7 @@ spec:
applicationsSync: create-update
```
### Ignore certain changes to Applications
## Ignore certain changes to Applications
The ApplicationSet spec includes an `ignoreApplicationDifferences` field, which allows you to specify which fields of
the ApplicationSet should be ignored when comparing Applications.
@@ -98,11 +98,94 @@ spec:
- jsonPointers:
- /spec/source/targetRevision
- name: some-app
jqExpressions:
jqPathExpressions:
- .spec.source.helm.values
```
### Prevent an `Application`'s child resources from being deleted, when the parent Application is deleted
### Allow temporarily toggling auto-sync
One of the most common use cases for ignoring differences is to allow temporarily toggling auto-sync for an Application.
For example, if you have an ApplicationSet that is configured to automatically sync Applications, you may want to temporarily
disable auto-sync for a specific Application. You can do this by adding an ignore rule for the `spec.syncPolicy.automated` field.
```yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
spec:
ignoreApplicationDifferences:
- jsonPointers:
- /spec/syncPolicy
```
### Limitations of `ignoreApplicationDifferences`
When an ApplicationSet is reconciled, the controller will compare the ApplicationSet spec with the spec of each Application
that it manages. If there are any differences, the controller will generate a patch to update the Application to match the
ApplicationSet spec.
The generated patch is a MergePatch. According to the MergePatch documentation, "existing lists will be completely
replaced by new lists" when there is a change to the list.
This limits the effectiveness of `ignoreApplicationDifferences` when the ignored field is in a list. For example, if you
have an application with multiple sources, and you want to ignore changes to the `targetRevision` of one of the sources,
changes in other fields or in other sources will cause the entire `sources` list to be replaced, and the `targetRevision`
field will be reset to the value defined in the ApplicationSet.
For example, consider this ApplicationSet:
```yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
spec:
ignoreApplicationDifferences:
- jqPathExpressions:
- .spec.sources[] | select(.repoURL == "https://git.example.com/org/repo1").targetRevision
template:
spec:
sources:
- repoURL: https://git.example.com/org/repo1
targetRevision: main
- repoURL: https://git.example.com/org/repo2
targetRevision: main
```
You can freely change the `targetRevision` of the `repo1` source, and the ApplicationSet controller will not overwrite
your change.
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
sources:
- repoURL: https://git.example.com/org/repo1
targetRevision: fix/bug-123
- repoURL: https://git.example.com/org/repo2
targetRevision: main
```
However, if you change the `targetRevision` of the `repo2` source, the ApplicationSet controller will overwrite the entire
`sources` field.
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
sources:
- repoURL: https://git.example.com/org/repo1
targetRevision: main
- repoURL: https://git.example.com/org/repo2
targetRevision: main
```
!!! note
[Future improvements](https://github.com/argoproj/argo-cd/issues/15975) to the ApplicationSet controller may
eliminate this problem. For example, the `ref` field might be made a merge key, allowing the ApplicationSet
controller to generate and use a StrategicMergePatch instead of a MergePatch. You could then target a specific
source by `ref`, ignore changes to a field in that source, and changes to other sources would not cause the ignored
field to be overwritten.
## Prevent an `Application`'s child resources from being deleted, when the parent Application is deleted
By default, when an `Application` resource is deleted by the ApplicationSet controller, all of the child resources of the Application will be deleted as well (such as, all of the Application's `Deployments`, `Services`, etc).
@@ -119,7 +202,7 @@ spec:
More information on the specific behaviour of `preserveResourcesOnDeletion`, and deletion in ApplicationSet controller and Argo CD in general, can be found on the [Application Deletion](Application-Deletion.md) page.
### Prevent an Application's child resources from being modified
## Prevent an Application's child resources from being modified
Changes made to the ApplicationSet will propagate to the Applications managed by the ApplicationSet, and then Argo CD will propagate the Application changes to the underlying cluster resources (as per [Argo CD Integration](Argo-CD-Integration.md)).
@@ -185,6 +268,11 @@ kubectl apply -n argocd -f install.yaml
## Preserving changes made to an Applications annotations and labels
!!! note
The same behavior can be achieved on a per-app basis using the [`ignoreApplicationDifferences`](#ignore-certain-changes-to-applications)
feature described above. However, preserved fields may be configured globally, a feature that is not yet available
for `ignoreApplicationDifferences`.
It is common practice in Kubernetes to store state in annotations, operators will often make use of this. To allow for this, it is possible to configure a list of annotations that the ApplicationSet should preserve when reconciling.
For example, imagine that we have an Application created from an ApplicationSet, but a custom annotation and label has since been added (to the Application) that does not exist in the `ApplicationSet` resource:
@@ -220,3 +308,18 @@ By default, the Argo CD notifications and the Argo CD refresh type annotations a
!!!note
One can also set global preserved fields for the controller by passing a comma separated list of annotations and labels to
`ARGOCD_APPLICATIONSET_CONTROLLER_GLOBAL_PRESERVED_ANNOTATIONS` and `ARGOCD_APPLICATIONSET_CONTROLLER_GLOBAL_PRESERVED_LABELS` respectively.
## Debugging unexpected changes to Applications
When the ApplicationSet controller makes a change to an application, it logs the patch at the debug level. To see these
logs, set the log level to debug in the `argocd-cmd-params-cm` ConfigMap in the `argocd` namespace:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cmd-params-cm
namespace: argocd
data:
applicationsetcontroller.log.level: debug
```

View File

@@ -174,6 +174,18 @@ It is also possible to use Sprig functions to construct the path variables manua
| `{{path.filenameNormalized}}` | `{{.path.filenameNormalized}}` | `{{normalize .path.filename}}` |
| `{{path[N]}}` | `-` | `{{index .path.segments N}}` |
## Available template functions
ApplicationSet controller provides:
- all [sprig](http://masterminds.github.io/sprig/) Go templates function except `env`, `expandenv` and `getHostByName`
- `normalize`: sanitizes the input so that it complies with the following rules:
1. contains no more than 253 characters
2. contains only lowercase alphanumeric characters, '-' or '.'
3. starts and ends with an alphanumeric character
- `toYaml` / `fromYaml` / `fromYamlArray` helm like functions
## Examples
### Basic Go template usage

View File

@@ -0,0 +1,5 @@
# 2.8 to 2.9
## Upgraded Kustomize Version
Note that bundled Kustomize version has been upgraded from 5.1.0 to 5.2.1.

View File

@@ -37,6 +37,7 @@ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/<v
<hr/>
* [v2.8 to v2.9](./2.8-2.9.md)
* [v2.7 to v2.8](./2.7-2.8.md)
* [v2.6 to v2.7](./2.6-2.7.md)
* [v2.5 to v2.6](./2.5-2.6.md)

View File

@@ -29,6 +29,7 @@ argocd admin dashboard [flags]
--proxy-url string If provided, this URL will be used to connect via proxy
--redis-compress string Enable this if the application controller is configured with redis compression enabled. (possible values: gzip, none) (default "gzip")
--request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0")
--server string The address and port of the Kubernetes API server
--tls-server-name string If provided, this name will be used to validate server certificate. If this is not provided, hostname used to contact the server is used.
--token string Bearer token for authentication to the API server
--user string The name of the kubeconfig user to use
@@ -58,7 +59,6 @@ argocd admin dashboard [flags]
--redis-haproxy-name string Name of the Redis HA Proxy; set this or the ARGOCD_REDIS_HAPROXY_NAME environment variable when the HA Proxy's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis-ha-haproxy")
--redis-name string Name of the Redis deployment; set this or the ARGOCD_REDIS_NAME environment variable when the Redis's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis")
--repo-server-name string Name of the Argo CD Repo server; set this or the ARGOCD_REPO_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-repo-server")
--server string Argo CD server address
--server-crt string Server certificate file
--server-name string Name of the Argo CD API server; set this or the ARGOCD_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-server")
```

8
go.mod
View File

@@ -77,11 +77,11 @@ require (
go.opentelemetry.io/otel v1.16.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0
go.opentelemetry.io/otel/sdk v1.16.0
golang.org/x/crypto v0.13.0
golang.org/x/crypto v0.14.0
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/oauth2 v0.11.0
golang.org/x/sync v0.3.0
golang.org/x/term v0.12.0
golang.org/x/term v0.13.0
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc
google.golang.org/grpc v1.56.2
google.golang.org/protobuf v1.31.0
@@ -261,8 +261,8 @@ require (
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/net v0.17.0
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.7.0 // indirect

12
go.sum
View File

@@ -2005,8 +2005,9 @@ golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -2158,8 +2159,9 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
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=
@@ -2351,8 +2353,9 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -2365,8 +2368,9 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
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=

View File

@@ -0,0 +1 @@
55a8e6dce87a1e52c61e0ce7a89bf85b38725ba3e8deb51d4a08ade8a2c70b2d helm-v3.13.2-linux-amd64.tar.gz

View File

@@ -0,0 +1 @@
f5654aaed63a0da72852776e1d3f851b2ea9529cb5696337202703c2e1ed2321 helm-v3.13.2-linux-arm64.tar.gz

View File

@@ -0,0 +1 @@
11d96134cc4ec106c23cd8c163072e9aed6cd73e36a3da120e5876d426203f37 helm-v3.13.2-linux-ppc64le.tar.gz

View File

@@ -0,0 +1 @@
3ffc5b4a041e5306dc00905ebe5dfea449e34ada268a713d34c69709afd6a9a2 helm-v3.13.2-linux-s390x.tar.gz

View File

@@ -11,7 +11,7 @@
# Use ./hack/installers/checksums/add-helm-checksums.sh and
# add-kustomize-checksums.sh to help download checksums.
###############################################################################
helm3_version=3.12.1
helm3_version=3.13.2
kubectl_version=1.17.8
kubectx_version=0.6.3
kustomize5_version=5.2.1

View File

@@ -5,7 +5,7 @@ kind: Kustomization
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: v2.9.0
newTag: v2.9.3
resources:
- ./application-controller
- ./dex

View File

@@ -20742,7 +20742,7 @@ spec:
key: applicationsetcontroller.allowed.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -21042,7 +21042,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -21094,7 +21094,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -21313,7 +21313,7 @@ spec:
key: controller.kubectl.parallelism.limit
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -12,4 +12,4 @@ resources:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: v2.9.0
newTag: v2.9.3

View File

@@ -12,7 +12,7 @@ patches:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: v2.9.0
newTag: v2.9.3
resources:
- ../../base/application-controller
- ../../base/applicationset-controller

View File

@@ -21999,7 +21999,7 @@ spec:
key: applicationsetcontroller.allowed.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -22122,7 +22122,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -22198,7 +22198,7 @@ spec:
key: application.namespaces
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -22529,7 +22529,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -22581,7 +22581,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -22870,7 +22870,7 @@ spec:
key: server.enable.proxy.extension
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -23116,7 +23116,7 @@ spec:
key: controller.kubectl.parallelism.limit
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -1654,7 +1654,7 @@ spec:
key: applicationsetcontroller.allowed.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -1777,7 +1777,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -1853,7 +1853,7 @@ spec:
key: application.namespaces
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -2184,7 +2184,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2236,7 +2236,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -2525,7 +2525,7 @@ spec:
key: server.enable.proxy.extension
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2771,7 +2771,7 @@ spec:
key: controller.kubectl.parallelism.limit
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -21094,7 +21094,7 @@ spec:
key: applicationsetcontroller.allowed.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -21217,7 +21217,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -21293,7 +21293,7 @@ spec:
key: application.namespaces
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -21575,7 +21575,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -21627,7 +21627,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -21914,7 +21914,7 @@ spec:
key: server.enable.proxy.extension
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -22160,7 +22160,7 @@ spec:
key: controller.kubectl.parallelism.limit
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -749,7 +749,7 @@ spec:
key: applicationsetcontroller.allowed.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -872,7 +872,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -948,7 +948,7 @@ spec:
key: application.namespaces
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -1230,7 +1230,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -1282,7 +1282,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -1569,7 +1569,7 @@ spec:
key: server.enable.proxy.extension
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -1815,7 +1815,7 @@ spec:
key: controller.kubectl.parallelism.limit
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.9.0
image: quay.io/argoproj/argocd:v2.9.3
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -123,6 +123,7 @@ nav:
- operator-manual/server-commands/additional-configuration-method.md
- Upgrading:
- operator-manual/upgrading/overview.md
- operator-manual/upgrading/2.8-2.9.md
- operator-manual/upgrading/2.7-2.8.md
- operator-manual/upgrading/2.6-2.7.md
- operator-manual/upgrading/2.5-2.6.md

View File

@@ -951,6 +951,35 @@ type ApplicationStatus struct {
ControllerNamespace string `json:"controllerNamespace,omitempty" protobuf:"bytes,13,opt,name=controllerNamespace"`
}
// GetRevisions will return the current revision associated with the Application.
// If app has multisources, it will return all corresponding revisions preserving
// order from the app.spec.sources. If app has only one source, it will return a
// single revision in the list.
func (a *ApplicationStatus) GetRevisions() []string {
revisions := []string{}
if len(a.Sync.Revisions) > 0 {
revisions = a.Sync.Revisions
} else if a.Sync.Revision != "" {
revisions = append(revisions, a.Sync.Revision)
}
return revisions
}
// BuildComparedToStatus will build a ComparedTo object based on the current
// Application state.
func (app *Application) BuildComparedToStatus() ComparedTo {
ct := ComparedTo{
Destination: app.Spec.Destination,
IgnoreDifferences: app.Spec.IgnoreDifferences,
}
if app.Spec.HasMultipleSources() {
ct.Sources = app.Spec.Sources
} else {
ct.Source = app.Spec.GetSource()
}
return ct
}
// JWTTokens represents a list of JWT tokens
type JWTTokens struct {
Items []JWTToken `json:"items,omitempty" protobuf:"bytes,1,opt,name=items"`
@@ -1135,11 +1164,12 @@ type SyncPolicy struct {
Retry *RetryStrategy `json:"retry,omitempty" protobuf:"bytes,3,opt,name=retry"`
// ManagedNamespaceMetadata controls metadata in the given namespace (if CreateNamespace=true)
ManagedNamespaceMetadata *ManagedNamespaceMetadata `json:"managedNamespaceMetadata,omitempty" protobuf:"bytes,4,opt,name=managedNamespaceMetadata"`
// If you add a field here, be sure to update IsZero.
}
// IsZero returns true if the sync policy is empty
func (p *SyncPolicy) IsZero() bool {
return p == nil || (p.Automated == nil && len(p.SyncOptions) == 0 && p.Retry == nil)
return p == nil || (p.Automated == nil && len(p.SyncOptions) == 0 && p.Retry == nil && p.ManagedNamespaceMetadata == nil)
}
// RetryStrategy contains information about the strategy to apply when a sync failed

View File

@@ -0,0 +1,71 @@
package mocks
import (
"testing"
"time"
"github.com/alicebob/miniredis/v2"
cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
cacheutilmocks "github.com/argoproj/argo-cd/v2/util/cache/mocks"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/mock"
)
type MockCacheType int
const (
MockCacheTypeRedis MockCacheType = iota
MockCacheTypeInMem
)
type MockRepoCache struct {
mock.Mock
RedisClient *cacheutilmocks.MockCacheClient
StopRedisCallback func()
}
type MockCacheOptions struct {
RepoCacheExpiration time.Duration
RevisionCacheExpiration time.Duration
ReadDelay time.Duration
WriteDelay time.Duration
}
type CacheCallCounts struct {
ExternalSets int
ExternalGets int
ExternalDeletes int
}
// Checks that the cache was called the expected number of times
func (mockCache *MockRepoCache) AssertCacheCalledTimes(t *testing.T, calls *CacheCallCounts) {
mockCache.RedisClient.AssertNumberOfCalls(t, "Get", calls.ExternalGets)
mockCache.RedisClient.AssertNumberOfCalls(t, "Set", calls.ExternalSets)
mockCache.RedisClient.AssertNumberOfCalls(t, "Delete", calls.ExternalDeletes)
}
func (mockCache *MockRepoCache) ConfigureDefaultCallbacks() {
mockCache.RedisClient.On("Get", mock.Anything, mock.Anything).Return(nil)
mockCache.RedisClient.On("Set", mock.Anything).Return(nil)
mockCache.RedisClient.On("Delete", mock.Anything).Return(nil)
}
func NewInMemoryRedis() (*redis.Client, func()) {
cacheutil.NewInMemoryCache(5 * time.Second)
mr, err := miniredis.Run()
if err != nil {
panic(err)
}
return redis.NewClient(&redis.Options{Addr: mr.Addr()}), mr.Close
}
func NewMockRepoCache(cacheOpts *MockCacheOptions) *MockRepoCache {
redisClient, stopRedis := NewInMemoryRedis()
redisCacheClient := &cacheutilmocks.MockCacheClient{
ReadDelay: cacheOpts.ReadDelay,
WriteDelay: cacheOpts.WriteDelay,
BaseCache: cacheutil.NewRedisCache(redisClient, cacheOpts.RepoCacheExpiration, cacheutil.RedisCompressionNone)}
newMockCache := &MockRepoCache{RedisClient: redisCacheClient, StopRedisCallback: stopRedis}
newMockCache.ConfigureDefaultCallbacks()
return newMockCache
}

View File

@@ -300,6 +300,7 @@ func (s *Service) runRepoOperation(
var gitClient git.Client
var helmClient helm.Client
var err error
gitClientOpts := git.WithCache(s.cache, !settings.noRevisionCache && !settings.noCache)
revision = textutils.FirstNonEmpty(revision, source.TargetRevision)
unresolvedRevision := revision
if source.IsHelm() {
@@ -308,13 +309,13 @@ func (s *Service) runRepoOperation(
return err
}
} else {
gitClient, revision, err = s.newClientResolveRevision(repo, revision, git.WithCache(s.cache, !settings.noRevisionCache && !settings.noCache))
gitClient, revision, err = s.newClientResolveRevision(repo, revision, gitClientOpts)
if err != nil {
return err
}
}
repoRefs, err := resolveReferencedSources(hasMultipleSources, source.Helm, refSources, s.newClientResolveRevision)
repoRefs, err := resolveReferencedSources(hasMultipleSources, source.Helm, refSources, s.newClientResolveRevision, gitClientOpts)
if err != nil {
return err
}
@@ -463,7 +464,7 @@ type gitClientGetter func(repo *v1alpha1.Repository, revision string, opts ...gi
//
// Much of this logic is duplicated in runManifestGenAsync. If making changes here, check whether runManifestGenAsync
// should be updated.
func resolveReferencedSources(hasMultipleSources bool, source *v1alpha1.ApplicationSourceHelm, refSources map[string]*v1alpha1.RefTarget, newClientResolveRevision gitClientGetter) (map[string]string, error) {
func resolveReferencedSources(hasMultipleSources bool, source *v1alpha1.ApplicationSourceHelm, refSources map[string]*v1alpha1.RefTarget, newClientResolveRevision gitClientGetter, gitClientOpts git.ClientOpts) (map[string]string, error) {
repoRefs := make(map[string]string)
if !hasMultipleSources || source == nil {
return repoRefs, nil
@@ -490,7 +491,7 @@ func resolveReferencedSources(hasMultipleSources bool, source *v1alpha1.Applicat
normalizedRepoURL := git.NormalizeGitURL(refSourceMapping.Repo.Repo)
_, ok = repoRefs[normalizedRepoURL]
if !ok {
_, referencedCommitSHA, err := newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision)
_, referencedCommitSHA, err := newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision, gitClientOpts)
if err != nil {
log.Errorf("Failed to get git client for repo %s: %v", refSourceMapping.Repo.Repo, err)
return nil, fmt.Errorf("failed to get git client for repo %s", refSourceMapping.Repo.Repo)
@@ -728,7 +729,7 @@ func (s *Service) runManifestGenAsync(ctx context.Context, repoRoot, commitSHA,
return
}
} else {
gitClient, referencedCommitSHA, err := s.newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision)
gitClient, referencedCommitSHA, err := s.newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision, git.WithCache(s.cache, !q.NoRevisionCache && !q.NoCache))
if err != nil {
log.Errorf("Failed to get git client for repo %s: %v", refSourceMapping.Repo.Repo, err)
ch.errCh <- fmt.Errorf("failed to get git client for repo %s", refSourceMapping.Repo.Repo)

View File

@@ -1,6 +1,7 @@
package repository
import (
"bytes"
"context"
"encoding/json"
"errors"
@@ -17,6 +18,7 @@ import (
"testing"
"time"
cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/api/resource"
@@ -28,13 +30,14 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/yaml"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
"github.com/argoproj/argo-cd/v2/reposerver/cache"
repositorymocks "github.com/argoproj/argo-cd/v2/reposerver/cache/mocks"
"github.com/argoproj/argo-cd/v2/reposerver/metrics"
fileutil "github.com/argoproj/argo-cd/v2/test/fixture/path"
"github.com/argoproj/argo-cd/v2/util/argo"
cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
dbmocks "github.com/argoproj/argo-cd/v2/util/db/mocks"
"github.com/argoproj/argo-cd/v2/util/git"
gitmocks "github.com/argoproj/argo-cd/v2/util/git/mocks"
@@ -51,12 +54,49 @@ gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>"
type clientFunc func(*gitmocks.Client, *helmmocks.Client, *iomocks.TempPaths)
func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client) {
type repoCacheMocks struct {
mock.Mock
cacheutilCache *cacheutil.Cache
cache *cache.Cache
mockCache *repositorymocks.MockRepoCache
}
type newGitRepoHelmChartOptions struct {
chartName string
chartVersion string
// valuesFiles is a map of the values file name to the key/value pairs to be written to the file
valuesFiles map[string]map[string]string
}
type newGitRepoOptions struct {
path string
createPath bool
remote string
addEmptyCommit bool
helmChartOptions newGitRepoHelmChartOptions
}
func newCacheMocks() *repoCacheMocks {
mockRepoCache := repositorymocks.NewMockRepoCache(&repositorymocks.MockCacheOptions{
RepoCacheExpiration: 1 * time.Minute,
RevisionCacheExpiration: 1 * time.Minute,
ReadDelay: 0,
WriteDelay: 0,
})
cacheutilCache := cacheutil.NewCache(mockRepoCache.RedisClient)
return &repoCacheMocks{
cacheutilCache: cacheutilCache,
cache: cache.NewCache(cacheutilCache, 1*time.Minute, 1*time.Minute),
mockCache: mockRepoCache,
}
}
func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *gitmocks.Client, *repoCacheMocks) {
root, err := filepath.Abs(root)
if err != nil {
panic(err)
}
return newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
return newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
gitClient.On("Init").Return(nil)
gitClient.On("Fetch", mock.Anything).Return(nil)
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
@@ -73,7 +113,7 @@ func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client)
chart := "my-chart"
oobChart := "out-of-bounds-chart"
version := "1.1.0"
helmClient.On("GetIndex", true).Return(&helm.Index{Entries: map[string]helm.Entries{
helmClient.On("GetIndex", mock.AnythingOfType("bool")).Return(&helm.Index{Entries: map[string]helm.Entries{
chart: {{Version: "1.0.0"}, {Version: version}},
oobChart: {{Version: "1.0.0"}, {Version: version}},
}}, nil)
@@ -89,18 +129,16 @@ func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client)
}, root)
}
func newServiceWithOpt(cf clientFunc, root string) (*Service, *gitmocks.Client) {
func newServiceWithOpt(t *testing.T, cf clientFunc, root string) (*Service, *gitmocks.Client, *repoCacheMocks) {
helmClient := &helmmocks.Client{}
gitClient := &gitmocks.Client{}
paths := &iomocks.TempPaths{}
cf(gitClient, helmClient, paths)
service := NewService(metrics.NewMetricsServer(), cache.NewCache(
cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)),
1*time.Minute,
1*time.Minute,
), RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, root)
cacheMocks := newCacheMocks()
t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, root)
service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, prosy string, opts ...git.ClientOpts) (client git.Client, e error) {
service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) {
return gitClient, nil
}
service.newHelmClient = func(repoURL string, creds helm.Creds, enableOci bool, proxy string, opts ...helm.ClientOpts) helm.Client {
@@ -110,20 +148,20 @@ func newServiceWithOpt(cf clientFunc, root string) (*Service, *gitmocks.Client)
return io.NopCloser
}
service.gitRepoPaths = paths
return service, gitClient
return service, gitClient, cacheMocks
}
func newService(root string) *Service {
service, _ := newServiceWithMocks(root, false)
func newService(t *testing.T, root string) *Service {
service, _, _ := newServiceWithMocks(t, root, false)
return service
}
func newServiceWithSignature(root string) *Service {
service, _ := newServiceWithMocks(root, true)
func newServiceWithSignature(t *testing.T, root string) *Service {
service, _, _ := newServiceWithMocks(t, root, true)
return service
}
func newServiceWithCommitSHA(root, revision string) *Service {
func newServiceWithCommitSHA(t *testing.T, root, revision string) *Service {
var revisionErr error
commitSHARegex := regexp.MustCompile("^[0-9A-Fa-f]{40}$")
@@ -131,7 +169,7 @@ func newServiceWithCommitSHA(root, revision string) *Service {
revisionErr = errors.New("not a commit SHA")
}
service, gitClient := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
service, gitClient, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
gitClient.On("Init").Return(nil)
gitClient.On("Fetch", mock.Anything).Return(nil)
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
@@ -150,7 +188,7 @@ func newServiceWithCommitSHA(root, revision string) *Service {
}
func TestGenerateYamlManifestInDir(t *testing.T) {
service := newService("../../manifests/base")
service := newService(t, "../../manifests/base")
src := argoappv1.ApplicationSource{Path: "."}
q := apiclient.ManifestRequest{
@@ -247,7 +285,7 @@ func TestGenerateManifests_MissingSymlinkDestination(t *testing.T) {
}
func TestGenerateManifests_K8SAPIResetCache(t *testing.T) {
service := newService("../../manifests/base")
service := newService(t, "../../manifests/base")
src := argoappv1.ApplicationSource{Path: "."}
q := apiclient.ManifestRequest{
@@ -275,7 +313,7 @@ func TestGenerateManifests_K8SAPIResetCache(t *testing.T) {
}
func TestGenerateManifests_EmptyCache(t *testing.T) {
service := newService("../../manifests/base")
service, gitMocks, mockCache := newServiceWithMocks(t, "../../manifests/base", false)
src := argoappv1.ApplicationSource{Path: "."}
q := apiclient.ManifestRequest{
@@ -291,11 +329,85 @@ func TestGenerateManifests_EmptyCache(t *testing.T) {
res, err := service.GenerateManifest(context.Background(), &q)
assert.NoError(t, err)
assert.True(t, len(res.Manifests) > 0)
mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
ExternalSets: 2,
ExternalGets: 2,
ExternalDeletes: 1})
gitMocks.AssertCalled(t, "LsRemote", mock.Anything)
gitMocks.AssertCalled(t, "Fetch", mock.Anything)
}
// Test that calling manifest generation on source helm reference helm files that when the revision is cached it does not call ls-remote
func TestGenerateManifestsHelmWithRefs_CachedNoLsRemote(t *testing.T) {
dir := t.TempDir()
repopath := fmt.Sprintf("%s/tmprepo", dir)
cacheMocks := newCacheMocks()
t.Cleanup(func() {
cacheMocks.mockCache.StopRedisCallback()
err := filepath.WalkDir(dir,
func(path string, di fs.DirEntry, err error) error {
if err == nil {
return os.Chmod(path, 0777)
}
return err
})
if err != nil {
t.Fatal(err)
}
})
service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, repopath)
var gitClient git.Client
var err error
service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) {
opts = append(opts, git.WithEventHandlers(git.EventHandlers{
// Primary check, we want to make sure ls-remote is not called when the item is in cache
OnLsRemote: func(repo string) func() {
return func() {
assert.Fail(t, "LsRemote should not be called when the item is in cache")
}
},
}))
gitClient, err = git.NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, opts...)
return gitClient, err
}
repoRemote := fmt.Sprintf("file://%s", repopath)
revision := initGitRepo(t, newGitRepoOptions{
path: repopath,
createPath: true,
remote: repoRemote,
helmChartOptions: newGitRepoHelmChartOptions{
chartName: "my-chart",
chartVersion: "v1.0.0",
valuesFiles: map[string]map[string]string{"test.yaml": {"testval": "test"}}},
})
src := argoappv1.ApplicationSource{RepoURL: repoRemote, Path: ".", TargetRevision: "HEAD", Helm: &argoappv1.ApplicationSourceHelm{
ValueFiles: []string{"$ref/test.yaml"},
}}
repo := &argoappv1.Repository{
Repo: repoRemote,
}
q := apiclient.ManifestRequest{
Repo: repo,
Revision: "HEAD",
HasMultipleSources: true,
ApplicationSource: &src,
ProjectName: "default",
ProjectSourceRepos: []string{"*"},
RefSources: map[string]*argoappv1.RefTarget{"$ref": {TargetRevision: "HEAD", Repo: *repo}},
}
err = cacheMocks.cacheutilCache.SetItem(fmt.Sprintf("git-refs|%s", repoRemote), [][2]string{{"HEAD", revision}}, 30*time.Second, false)
assert.NoError(t, err)
_, err = service.GenerateManifest(context.Background(), &q)
assert.NoError(t, err)
cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
ExternalSets: 2,
ExternalGets: 5})
}
// ensure we can use a semver constraint range (>= 1.0.0) and get back the correct chart (1.0.0)
func TestHelmManifestFromChartRepo(t *testing.T) {
service := newService(".")
root := t.TempDir()
service, gitMocks, mockCache := newServiceWithMocks(t, root, false)
source := &argoappv1.ApplicationSource{Chart: "my-chart", TargetRevision: ">= 1.0.0"}
request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something",
ProjectSourceRepos: []string{"*"}}
@@ -309,10 +421,14 @@ func TestHelmManifestFromChartRepo(t *testing.T) {
Revision: "1.1.0",
SourceType: "Helm",
}, response)
mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
ExternalSets: 1,
ExternalGets: 0})
gitMocks.AssertNotCalled(t, "LsRemote", mock.Anything)
}
func TestHelmChartReferencingExternalValues(t *testing.T) {
service := newService(".")
service := newService(t, ".")
spec := argoappv1.ApplicationSpec{
Sources: []argoappv1.ApplicationSource{
{RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &argoappv1.ApplicationSourceHelm{
@@ -342,7 +458,7 @@ func TestHelmChartReferencingExternalValues(t *testing.T) {
}
func TestHelmChartReferencingExternalValues_OutOfBounds_Symlink(t *testing.T) {
service := newService(".")
service := newService(t, ".")
err := os.Mkdir("testdata/oob-symlink", 0755)
require.NoError(t, err)
t.Cleanup(func() {
@@ -376,7 +492,7 @@ func TestHelmChartReferencingExternalValues_OutOfBounds_Symlink(t *testing.T) {
}
func TestGenerateManifestsUseExactRevision(t *testing.T) {
service, gitClient := newServiceWithMocks(".", false)
service, gitClient, _ := newServiceWithMocks(t, ".", false)
src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
@@ -390,7 +506,7 @@ func TestGenerateManifestsUseExactRevision(t *testing.T) {
}
func TestRecurseManifestsInDir(t *testing.T) {
service := newService(".")
service := newService(t, ".")
src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
@@ -403,7 +519,7 @@ func TestRecurseManifestsInDir(t *testing.T) {
}
func TestInvalidManifestsInDir(t *testing.T) {
service := newService(".")
service := newService(t, ".")
src := argoappv1.ApplicationSource{Path: "./testdata/invalid-manifests", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
@@ -414,7 +530,7 @@ func TestInvalidManifestsInDir(t *testing.T) {
}
func TestInvalidMetadata(t *testing.T) {
service := newService(".")
service := newService(t, ".")
src := argoappv1.ApplicationSource{Path: "./testdata/invalid-metadata", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, AppLabelKey: "test", AppName: "invalid-metadata", TrackingMethod: "annotation+label"}
@@ -424,7 +540,7 @@ func TestInvalidMetadata(t *testing.T) {
}
func TestNilMetadataAccessors(t *testing.T) {
service := newService(".")
service := newService(t, ".")
expected := "{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{\"argocd.argoproj.io/tracking-id\":\"nil-metadata-accessors:/ConfigMap:/my-map\"},\"labels\":{\"test\":\"nil-metadata-accessors\"},\"name\":\"my-map\"},\"stringData\":{\"foo\":\"bar\"}}"
src := argoappv1.ApplicationSource{Path: "./testdata/nil-metadata-accessors", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
@@ -436,7 +552,7 @@ func TestNilMetadataAccessors(t *testing.T) {
}
func TestGenerateJsonnetManifestInDir(t *testing.T) {
service := newService(".")
service := newService(t, ".")
q := apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
@@ -459,7 +575,7 @@ func TestGenerateJsonnetManifestInDir(t *testing.T) {
}
func TestGenerateJsonnetManifestInRootDir(t *testing.T) {
service := newService("testdata/jsonnet-1")
service := newService(t, "testdata/jsonnet-1")
q := apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
@@ -482,7 +598,7 @@ func TestGenerateJsonnetManifestInRootDir(t *testing.T) {
}
func TestGenerateJsonnetLibOutside(t *testing.T) {
service := newService(".")
service := newService(t, ".")
q := apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
@@ -553,7 +669,7 @@ func TestManifestGenErrorCacheByNumRequests(t *testing.T) {
for _, tt := range tests {
testName := fmt.Sprintf("gen-attempts-%d-pause-%d-total-%d", tt.PauseGenerationAfterFailedGenerationAttempts, tt.PauseGenerationOnFailureForRequests, tt.TotalCacheInvocations)
t.Run(testName, func(t *testing.T) {
service := newService(".")
service := newService(t, ".")
service.initConstants = RepoServerInitConstants{
ParallelismLimit: 1,
@@ -631,7 +747,7 @@ func TestManifestGenErrorCacheFileContentsChange(t *testing.T) {
tmpDir := t.TempDir()
service := newService(tmpDir)
service := newService(t, tmpDir)
service.initConstants = RepoServerInitConstants{
ParallelismLimit: 1,
@@ -701,7 +817,7 @@ func TestManifestGenErrorCacheByMinutesElapsed(t *testing.T) {
for _, tt := range tests {
testName := fmt.Sprintf("pause-time-%d", tt.PauseGenerationOnFailureForMinutes)
t.Run(testName, func(t *testing.T) {
service := newService(".")
service := newService(t, ".")
// Here we simulate the passage of time by overriding the now() function of Service
currentTime := time.Now()
@@ -771,7 +887,7 @@ func TestManifestGenErrorCacheByMinutesElapsed(t *testing.T) {
func TestManifestGenErrorCacheRespectsNoCache(t *testing.T) {
service := newService(".")
service := newService(t, ".")
service.initConstants = RepoServerInitConstants{
ParallelismLimit: 1,
@@ -828,7 +944,7 @@ func TestManifestGenErrorCacheRespectsNoCache(t *testing.T) {
}
func TestGenerateHelmWithValues(t *testing.T) {
service := newService("../../util/helm/testdata/redis")
service := newService(t, "../../util/helm/testdata/redis")
res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
@@ -865,7 +981,7 @@ func TestGenerateHelmWithValues(t *testing.T) {
}
func TestHelmWithMissingValueFiles(t *testing.T) {
service := newService("../../util/helm/testdata/redis")
service := newService(t, "../../util/helm/testdata/redis")
missingValuesFile := "values-prod-overrides.yaml"
req := &apiclient.ManifestRequest{
@@ -893,7 +1009,7 @@ func TestHelmWithMissingValueFiles(t *testing.T) {
}
func TestGenerateHelmWithEnvVars(t *testing.T) {
service := newService("../../util/helm/testdata/redis")
service := newService(t, "../../util/helm/testdata/redis")
res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
@@ -930,7 +1046,7 @@ func TestGenerateHelmWithEnvVars(t *testing.T) {
// The requested value file (`../minio/values.yaml`) is outside the app path (`./util/helm/testdata/redis`), however
// since the requested value is still under the repo directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed
func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) {
service := newService("../../util/helm/testdata")
service := newService(t, "../../util/helm/testdata")
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
AppName: "test",
@@ -947,7 +1063,7 @@ func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) {
assert.NoError(t, err)
// Test the case where the path is "."
service = newService("./testdata")
service = newService(t, "./testdata")
_, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
AppName: "test",
@@ -961,7 +1077,7 @@ func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) {
}
func TestChartRepoWithOutOfBoundsSymlink(t *testing.T) {
service := newService(".")
service := newService(t, ".")
source := &argoappv1.ApplicationSource{Chart: "out-of-bounds-chart", TargetRevision: ">= 1.0.0"}
request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true}
_, err := service.GenerateManifest(context.Background(), request)
@@ -971,7 +1087,7 @@ func TestChartRepoWithOutOfBoundsSymlink(t *testing.T) {
// This is a Helm first-class app with a values file inside the repo directory
// (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is allowed
func TestHelmManifestFromChartRepoWithValueFile(t *testing.T) {
service := newService(".")
service := newService(t, ".")
source := &argoappv1.ApplicationSource{
Chart: "my-chart",
TargetRevision: ">= 1.0.0",
@@ -1000,7 +1116,7 @@ func TestHelmManifestFromChartRepoWithValueFile(t *testing.T) {
// This is a Helm first-class app with a values file outside the repo directory
// (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is not allowed
func TestHelmManifestFromChartRepoWithValueFileOutsideRepo(t *testing.T) {
service := newService(".")
service := newService(t, ".")
source := &argoappv1.ApplicationSource{
Chart: "my-chart",
TargetRevision: ">= 1.0.0",
@@ -1015,7 +1131,7 @@ func TestHelmManifestFromChartRepoWithValueFileOutsideRepo(t *testing.T) {
func TestHelmManifestFromChartRepoWithValueFileLinks(t *testing.T) {
t.Run("Valid symlink", func(t *testing.T) {
service := newService(".")
service := newService(t, ".")
source := &argoappv1.ApplicationSource{
Chart: "my-chart",
TargetRevision: ">= 1.0.0",
@@ -1031,7 +1147,7 @@ func TestHelmManifestFromChartRepoWithValueFileLinks(t *testing.T) {
}
func TestGenerateHelmWithURL(t *testing.T) {
service := newService("../../util/helm/testdata/redis")
service := newService(t, "../../util/helm/testdata/redis")
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
@@ -1054,7 +1170,7 @@ func TestGenerateHelmWithURL(t *testing.T) {
// (`~/go/src/github.com/argoproj/argo-cd/util/helm/testdata/redis`), so it is blocked
func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
t.Run("Values file with relative path pointing outside repo root", func(t *testing.T) {
service := newService("../../util/helm/testdata/redis")
service := newService(t, "../../util/helm/testdata/redis")
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
AppName: "test",
@@ -1073,7 +1189,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
})
t.Run("Values file with relative path pointing inside repo root", func(t *testing.T) {
service := newService("./testdata")
service := newService(t, "./testdata")
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
AppName: "test",
@@ -1091,7 +1207,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
})
t.Run("Values file with absolute path stays within repo root", func(t *testing.T) {
service := newService("./testdata")
service := newService(t, "./testdata")
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
AppName: "test",
@@ -1109,7 +1225,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
})
t.Run("Values file with absolute path using back-references outside repo root", func(t *testing.T) {
service := newService("./testdata")
service := newService(t, "./testdata")
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
AppName: "test",
@@ -1128,7 +1244,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
})
t.Run("Remote values file from forbidden protocol", func(t *testing.T) {
service := newService("./testdata")
service := newService(t, "./testdata")
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
AppName: "test",
@@ -1147,7 +1263,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
})
t.Run("Remote values file from custom allowed protocol", func(t *testing.T) {
service := newService("./testdata")
service := newService(t, "./testdata")
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
AppName: "test",
@@ -1168,7 +1284,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
// File parameter should not allow traversal outside of the repository root
func TestGenerateHelmWithAbsoluteFileParameter(t *testing.T) {
service := newService("../..")
service := newService(t, "../..")
file, err := os.CreateTemp("", "external-secret.txt")
assert.NoError(t, err)
@@ -1209,7 +1325,7 @@ func TestGenerateHelmWithAbsoluteFileParameter(t *testing.T) {
// directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed. It is used as a means of
// providing direct content to a helm chart via a specific key.
func TestGenerateHelmWithFileParameter(t *testing.T) {
service := newService("../../util/helm/testdata")
service := newService(t, "../../util/helm/testdata")
res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
@@ -1234,7 +1350,7 @@ func TestGenerateHelmWithFileParameter(t *testing.T) {
}
func TestGenerateNullList(t *testing.T) {
service := newService(".")
service := newService(t, ".")
t.Run("null list", func(t *testing.T) {
res1, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
@@ -1302,7 +1418,7 @@ func TestGenerateFromUTF16(t *testing.T) {
}
func TestListApps(t *testing.T) {
service := newService("./testdata")
service := newService(t, "./testdata")
res, err := service.ListApps(context.Background(), &apiclient.ListAppsRequest{Repo: &argoappv1.Repository{}})
assert.NoError(t, err)
@@ -1329,7 +1445,7 @@ func TestListApps(t *testing.T) {
}
func TestGetAppDetailsHelm(t *testing.T) {
service := newService("../../util/helm/testdata/dependency")
service := newService(t, "../../util/helm/testdata/dependency")
res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
Repo: &argoappv1.Repository{},
@@ -1344,8 +1460,26 @@ func TestGetAppDetailsHelm(t *testing.T) {
assert.Equal(t, "Helm", res.Type)
assert.EqualValues(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles)
}
func TestGetAppDetailsHelmUsesCache(t *testing.T) {
service := newService(t, "../../util/helm/testdata/dependency")
res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
Repo: &argoappv1.Repository{},
Source: &argoappv1.ApplicationSource{
Path: ".",
},
})
assert.NoError(t, err)
assert.NotNil(t, res.Helm)
assert.Equal(t, "Helm", res.Type)
assert.EqualValues(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles)
}
func TestGetAppDetailsHelm_WithNoValuesFile(t *testing.T) {
service := newService("../../util/helm/testdata/api-versions")
service := newService(t, "../../util/helm/testdata/api-versions")
res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
Repo: &argoappv1.Repository{},
@@ -1363,7 +1497,7 @@ func TestGetAppDetailsHelm_WithNoValuesFile(t *testing.T) {
}
func TestGetAppDetailsKustomize(t *testing.T) {
service := newService("../../util/kustomize/testdata/kustomization_yaml")
service := newService(t, "../../util/kustomize/testdata/kustomization_yaml")
res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
Repo: &argoappv1.Repository{},
@@ -1380,7 +1514,7 @@ func TestGetAppDetailsKustomize(t *testing.T) {
}
func TestGetHelmCharts(t *testing.T) {
service := newService("../..")
service := newService(t, "../..")
res, err := service.GetHelmCharts(context.Background(), &apiclient.HelmChartsRequest{Repo: &argoappv1.Repository{}})
// fix flakiness
@@ -1401,7 +1535,7 @@ func TestGetHelmCharts(t *testing.T) {
}
func TestGetRevisionMetadata(t *testing.T) {
service, gitClient := newServiceWithMocks("../..", false)
service, gitClient, _ := newServiceWithMocks(t, "../..", false)
now := time.Now()
gitClient.On("RevisionMetadata", mock.Anything).Return(&git.RevisionMetadata{
@@ -1469,7 +1603,7 @@ func TestGetRevisionMetadata(t *testing.T) {
func TestGetSignatureVerificationResult(t *testing.T) {
// Commit with signature and verification requested
{
service := newServiceWithSignature("../../manifests/base")
service := newServiceWithSignature(t, "../../manifests/base")
src := argoappv1.ApplicationSource{Path: "."}
q := apiclient.ManifestRequest{
@@ -1486,7 +1620,7 @@ func TestGetSignatureVerificationResult(t *testing.T) {
}
// Commit with signature and verification not requested
{
service := newServiceWithSignature("../../manifests/base")
service := newServiceWithSignature(t, "../../manifests/base")
src := argoappv1.ApplicationSource{Path: "."}
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, ProjectName: "something",
@@ -1498,7 +1632,7 @@ func TestGetSignatureVerificationResult(t *testing.T) {
}
// Commit without signature and verification requested
{
service := newService("../../manifests/base")
service := newService(t, "../../manifests/base")
src := argoappv1.ApplicationSource{Path: "."}
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
@@ -1510,7 +1644,7 @@ func TestGetSignatureVerificationResult(t *testing.T) {
}
// Commit without signature and verification not requested
{
service := newService("../../manifests/base")
service := newService(t, "../../manifests/base")
src := argoappv1.ApplicationSource{Path: "."}
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
@@ -1543,7 +1677,7 @@ func Test_newEnv(t *testing.T) {
}
func TestService_newHelmClientResolveRevision(t *testing.T) {
service := newService(".")
service := newService(t, ".")
t.Run("EmptyRevision", func(t *testing.T) {
_, _, err := service.newHelmClientResolveRevision(&argoappv1.Repository{}, "", "", true)
@@ -1557,7 +1691,7 @@ func TestService_newHelmClientResolveRevision(t *testing.T) {
func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
t.Run("No app name set and app specific file exists", func(t *testing.T) {
service := newService(".")
service := newService(t, ".")
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
Repo: &argoappv1.Repository{},
@@ -1570,7 +1704,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
})
})
t.Run("No app specific override", func(t *testing.T) {
service := newService(".")
service := newService(t, ".")
runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
Repo: &argoappv1.Repository{},
@@ -1584,7 +1718,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
})
})
t.Run("Only app specific override", func(t *testing.T) {
service := newService(".")
service := newService(t, ".")
runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
Repo: &argoappv1.Repository{},
@@ -1598,7 +1732,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
})
})
t.Run("App specific override", func(t *testing.T) {
service := newService(".")
service := newService(t, ".")
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
Repo: &argoappv1.Repository{},
@@ -1612,7 +1746,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
})
})
t.Run("App specific overrides containing non-mergeable field", func(t *testing.T) {
service := newService(".")
service := newService(t, ".")
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
Repo: &argoappv1.Repository{},
@@ -1626,7 +1760,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
})
})
t.Run("Broken app-specific overrides", func(t *testing.T) {
service := newService(".")
service := newService(t, ".")
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
_, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
Repo: &argoappv1.Repository{},
@@ -1668,7 +1802,7 @@ func runWithTempTestdata(t *testing.T, path string, runner func(t *testing.T, pa
func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
t.Run("Single global override", func(t *testing.T) {
runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
service := newService(".")
service := newService(t, ".")
manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
ApplicationSource: &argoappv1.ApplicationSource{
@@ -1699,7 +1833,7 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
t.Run("Single global override Helm", func(t *testing.T) {
runWithTempTestdata(t, "single-global-helm", func(t *testing.T, path string) {
service := newService(".")
service := newService(t, ".")
manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
ApplicationSource: &argoappv1.ApplicationSource{
@@ -1729,7 +1863,7 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
})
t.Run("Application specific override", func(t *testing.T) {
service := newService(".")
service := newService(t, ".")
runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
@@ -1760,8 +1894,29 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
})
})
t.Run("Multi-source with source as ref only does not generate manifests", func(t *testing.T) {
service := newService(t, ".")
runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
ApplicationSource: &argoappv1.ApplicationSource{
Path: "",
Chart: "",
Ref: "test",
},
AppName: "testapp-multi-ref-only",
ProjectName: "something",
ProjectSourceRepos: []string{"*"},
HasMultipleSources: true,
})
assert.NoError(t, err)
assert.Empty(t, manifests.Manifests)
assert.NotEmpty(t, manifests.Revision)
})
})
t.Run("Application specific override for other app", func(t *testing.T) {
service := newService(".")
service := newService(t, ".")
runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
@@ -1793,7 +1948,7 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
})
t.Run("Override info does not appear in cache key", func(t *testing.T) {
service := newService(".")
service := newService(t, ".")
runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
source := &argoappv1.ApplicationSource{
Path: path,
@@ -1843,7 +1998,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) {
ProjectSourceRepos: []string{"*"},
},
wantError: false,
service: newServiceWithCommitSHA(".", regularGitTagHash),
service: newServiceWithCommitSHA(t, ".", regularGitTagHash),
},
{
@@ -1859,7 +2014,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) {
ProjectSourceRepos: []string{"*"},
},
wantError: false,
service: newServiceWithCommitSHA(".", annotatedGitTaghash),
service: newServiceWithCommitSHA(t, ".", annotatedGitTaghash),
},
{
@@ -1875,7 +2030,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) {
ProjectSourceRepos: []string{"*"},
},
wantError: true,
service: newServiceWithCommitSHA(".", invalidGitTaghash),
service: newServiceWithCommitSHA(t, ".", invalidGitTaghash),
},
}
for _, tt := range tests {
@@ -1900,7 +2055,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) {
func TestGenerateManifestWithAnnotatedTagsAndMultiSourceApp(t *testing.T) {
annotatedGitTaghash := "95249be61b028d566c29d47b19e65c5603388a41"
service := newServiceWithCommitSHA(".", annotatedGitTaghash)
service := newServiceWithCommitSHA(t, ".", annotatedGitTaghash)
refSources := map[string]*argoappv1.RefTarget{}
@@ -2485,7 +2640,7 @@ func Test_findManifests(t *testing.T) {
}
func TestTestRepoOCI(t *testing.T) {
service := newService(".")
service := newService(t, ".")
_, err := service.TestRepository(context.Background(), &apiclient.TestRepositoryRequest{
Repo: &argoappv1.Repository{
Repo: "https://demo.goharbor.io",
@@ -2510,7 +2665,7 @@ func Test_getHelmDependencyRepos(t *testing.T) {
func TestResolveRevision(t *testing.T) {
service := newService(".")
service := newService(t, ".")
repo := &argoappv1.Repository{Repo: "https://github.com/argoproj/argo-cd"}
app := &argoappv1.Application{Spec: argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{}}}
resolveRevisionResponse, err := service.ResolveRevision(context.Background(), &apiclient.ResolveRevisionRequest{
@@ -2532,7 +2687,7 @@ func TestResolveRevision(t *testing.T) {
func TestResolveRevisionNegativeScenarios(t *testing.T) {
service := newService(".")
service := newService(t, ".")
repo := &argoappv1.Repository{Repo: "https://github.com/argoproj/argo-cd"}
app := &argoappv1.Application{Spec: argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{}}}
resolveRevisionResponse, err := service.ResolveRevision(context.Background(), &apiclient.ResolveRevisionRequest{
@@ -2579,19 +2734,57 @@ func TestDirectoryPermissionInitializer(t *testing.T) {
require.Error(t, err)
}
func initGitRepo(repoPath string, remote string) error {
if err := os.Mkdir(repoPath, 0755); err != nil {
return err
func addHelmToGitRepo(t *testing.T, options newGitRepoOptions) {
err := os.WriteFile(filepath.Join(options.path, "Chart.yaml"), []byte("name: test\nversion: v1.0.0"), 0777)
assert.NoError(t, err)
for valuesFileName, values := range options.helmChartOptions.valuesFiles {
valuesFileContents, err := yaml.Marshal(values)
assert.NoError(t, err)
err = os.WriteFile(filepath.Join(options.path, valuesFileName), valuesFileContents, 0777)
assert.NoError(t, err)
}
assert.NoError(t, err)
cmd := exec.Command("git", "add", "-A")
cmd.Dir = options.path
assert.NoError(t, cmd.Run())
cmd = exec.Command("git", "commit", "-m", "Initial commit")
cmd.Dir = options.path
assert.NoError(t, cmd.Run())
}
func initGitRepo(t *testing.T, options newGitRepoOptions) (revision string) {
if options.createPath {
assert.NoError(t, os.Mkdir(options.path, 0755))
}
cmd := exec.Command("git", "init", repoPath)
cmd.Dir = repoPath
if err := cmd.Run(); err != nil {
return err
cmd := exec.Command("git", "init", options.path)
cmd.Dir = options.path
assert.NoError(t, cmd.Run())
if options.remote != "" {
cmd = exec.Command("git", "remote", "add", "origin", options.path)
cmd.Dir = options.path
assert.NoError(t, cmd.Run())
}
cmd = exec.Command("git", "remote", "add", "origin", remote)
cmd.Dir = repoPath
return cmd.Run()
commitAdded := options.addEmptyCommit || options.helmChartOptions.chartName != ""
if options.addEmptyCommit {
cmd = exec.Command("git", "commit", "-m", "Initial commit", "--allow-empty")
cmd.Dir = options.path
assert.NoError(t, cmd.Run())
} else if options.helmChartOptions.chartName != "" {
addHelmToGitRepo(t, options)
}
if commitAdded {
var revB bytes.Buffer
cmd = exec.Command("git", "rev-parse", "HEAD", options.path)
cmd.Dir = options.path
cmd.Stdout = &revB
assert.NoError(t, cmd.Run())
revision = strings.Split(revB.String(), "\n")[0]
}
return revision
}
func TestInit(t *testing.T) {
@@ -2604,16 +2797,16 @@ func TestInit(t *testing.T) {
})
repoPath := path.Join(dir, "repo1")
require.NoError(t, initGitRepo(repoPath, "https://github.com/argo-cd/test-repo1"))
initGitRepo(t, newGitRepoOptions{path: repoPath, remote: "https://github.com/argo-cd/test-repo1", createPath: true, addEmptyCommit: false})
service := newService(".")
service := newService(t, ".")
service.rootDir = dir
require.NoError(t, service.Init())
_, err := os.ReadDir(dir)
require.Error(t, err)
require.NoError(t, initGitRepo(path.Join(dir, "repo2"), "https://github.com/argo-cd/test-repo2"))
initGitRepo(t, newGitRepoOptions{path: path.Join(dir, "repo2"), remote: "https://github.com/argo-cd/test-repo2", createPath: true, addEmptyCommit: false})
}
// TestCheckoutRevisionCanGetNonstandardRefs shows that we can fetch a revision that points to a non-standard ref. In
@@ -2915,7 +3108,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
want *apiclient.GitDirectoriesResponse
wantErr assert.ErrorAssertionFunc
}{
{name: "InvalidRepo", fields: fields{service: newService(".")}, args: args{
{name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{
ctx: context.TODO(),
request: &apiclient.GitDirectoriesRequest{
Repo: nil,
@@ -2924,7 +3117,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
},
}, want: nil, wantErr: assert.Error},
{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error"))
paths.On("GetPath", mock.Anything).Return(".", nil)
@@ -2955,7 +3148,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
func TestGetGitDirectories(t *testing.T) {
// test not using the cache
root := "./testdata/git-files-dirs"
s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
gitClient.On("Init").Return(nil)
gitClient.On("Fetch", mock.Anything).Return(nil)
gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return(nil)
@@ -2978,6 +3171,10 @@ func TestGetGitDirectories(t *testing.T) {
directories, err = s.GetGitDirectories(context.TODO(), dirRequest)
assert.Nil(t, err)
assert.ElementsMatch(t, []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo"}, directories.GetPaths())
cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
ExternalSets: 1,
ExternalGets: 2,
})
}
func TestErrorGetGitFiles(t *testing.T) {
@@ -2995,7 +3192,7 @@ func TestErrorGetGitFiles(t *testing.T) {
want *apiclient.GitFilesResponse
wantErr assert.ErrorAssertionFunc
}{
{name: "InvalidRepo", fields: fields{service: newService(".")}, args: args{
{name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{
ctx: context.TODO(),
request: &apiclient.GitFilesRequest{
Repo: nil,
@@ -3004,7 +3201,7 @@ func TestErrorGetGitFiles(t *testing.T) {
},
}, want: nil, wantErr: assert.Error},
{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error"))
paths.On("GetPath", mock.Anything).Return(".", nil)
@@ -3037,7 +3234,7 @@ func TestGetGitFiles(t *testing.T) {
files := []string{"./testdata/git-files-dirs/somedir/config.yaml",
"./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/app/foo/bar/config.yaml"}
root := ""
s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
gitClient.On("Init").Return(nil)
gitClient.On("Fetch", mock.Anything).Return(nil)
gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return(nil)
@@ -3070,6 +3267,10 @@ func TestGetGitFiles(t *testing.T) {
fileResponse, err = s.GetGitFiles(context.TODO(), filesRequest)
assert.Nil(t, err)
assert.Equal(t, expected, fileResponse.GetMap())
cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
ExternalSets: 1,
ExternalGets: 2,
})
}
func Test_getRepoSanitizerRegex(t *testing.T) {
@@ -3079,3 +3280,45 @@ func Test_getRepoSanitizerRegex(t *testing.T) {
msg = r.ReplaceAllString("error message containing /tmp/_argocd-repo/SENSITIVE/with/trailing/path and other stuff", "<path to cached source>")
assert.Equal(t, "error message containing <path to cached source>/with/trailing/path and other stuff", msg)
}
func TestGetRevisionChartDetails(t *testing.T) {
t.Run("Test revision semvar", func(t *testing.T) {
root := t.TempDir()
service := newService(t, root)
_, err := service.GetRevisionChartDetails(context.Background(), &apiclient.RepoServerRevisionChartDetailsRequest{
Repo: &v1alpha1.Repository{
Repo: fmt.Sprintf("file://%s", root),
Name: "test-repo-name",
Type: "helm",
},
Name: "test-name",
Revision: "test-revision",
})
assert.ErrorContains(t, err, "invalid revision")
})
t.Run("Test GetRevisionChartDetails", func(t *testing.T) {
root := t.TempDir()
service := newService(t, root)
repoUrl := fmt.Sprintf("file://%s", root)
err := service.cache.SetRevisionChartDetails(repoUrl, "my-chart", "1.1.0", &argoappv1.ChartDetails{
Description: "test-description",
Home: "test-home",
Maintainers: []string{"test-maintainer"},
})
assert.NoError(t, err)
chartDetails, err := service.GetRevisionChartDetails(context.Background(), &apiclient.RepoServerRevisionChartDetailsRequest{
Repo: &v1alpha1.Repository{
Repo: fmt.Sprintf("file://%s", root),
Name: "test-repo-name",
Type: "helm",
},
Name: "my-chart",
Revision: "1.1.0",
})
assert.NoError(t, err)
assert.Equal(t, "test-description", chartDetails.Description)
assert.Equal(t, "test-home", chartDetails.Home)
assert.Equal(t, []string{"test-maintainer"}, chartDetails.Maintainers)
})
}

View File

@@ -1,12 +1,11 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testEnvironment: 'jsdom',
reporters: ['default', 'jest-junit'],
collectCoverage: true,
transformIgnorePatterns: ['node_modules/(?!(argo-ui)/)'],
globals: {
'self': {},
'window': {localStorage: { getItem: () => '{}', setItem: () => null }},
'ts-jest': {
isolatedModules: true,
},
@@ -17,20 +16,3 @@ module.exports = {
'.+\\.(css|styl|less|sass|scss)$': 'jest-transform-css',
},
};
const localStorageMock = (() => {
let store = {};
return {
getItem: (key) => store[key],
setItem: (key, value) => {
store[key] = value.toString();
},
clear: () => {
store = {};
},
removeItem: (key) => {
delete store[key];
}
};
})();
global.localStorage = localStorageMock;

View File

@@ -8,16 +8,16 @@ $header: 120px;
.application-details {
height: 100vh;
width: 100%;
&__status-panel {
position: fixed;
left: $sidebar-width;
right: 0;
z-index: 3;
@media screen and (max-width: map-get($breakpoints, xlarge)) {
top: 150px;
}
@media screen and (max-width: map-get($breakpoints, large)) {
top: 146px;
&__wrapper {
display: flex;
flex-direction: column;
height: calc(100vh - 2 * $top-bar-height);
overflow: hidden;
@media screen and (max-width: map-get($breakpoints, xxlarge)) {
height: calc(100vh - 3 * $top-bar-height);
margin-top: $top-bar-height;
}
}
@@ -27,13 +27,11 @@ $header: 120px;
&__tree {
padding: 1em;
flex: 1;
overflow-x: auto;
overflow-y: auto;
margin-top: 150px;
height: calc(100vh - 2 * 70px - 115px);
@media screen and (max-width: map-get($breakpoints, xlarge)) {
margin-top: 165px;
}
overscroll-behavior-x: none;
}
&__sliding-panel-pagination-wrap {

View File

@@ -416,125 +416,20 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
</React.Fragment>
)
}}>
<div className='application-details__status-panel'>
<ApplicationStatusPanel
application={application}
showDiff={() => this.selectNode(appFullName, 0, 'diff')}
showOperation={() => this.setOperationStatusVisible(true)}
showConditions={() => this.setConditionsStatusVisible(true)}
showMetadataInfo={revision => this.setState({...this.state, revision})}
/>
</div>
<div className='application-details__tree'>
{refreshing && <p className='application-details__refreshing-label'>Refreshing</p>}
{((pref.view === 'tree' || pref.view === 'network') && (
<>
<DataLoader load={() => services.viewPreferences.getPreferences()}>
{viewPref => (
<ApplicationDetailsFilters
pref={pref}
tree={tree}
onSetFilter={setFilter}
onClearFilter={clearFilter}
collapsed={viewPref.hideSidebar}
resourceNodes={this.state.filteredGraph}
/>
)}
</DataLoader>
<div className='graph-options-panel'>
<a
className={`group-nodes-button`}
onClick={() => {
toggleNameDirection();
}}
title={this.state.truncateNameOnRight ? 'Truncate resource name right' : 'Truncate resource name left'}>
<i
className={classNames({
'fa fa-align-right': this.state.truncateNameOnRight,
'fa fa-align-left': !this.state.truncateNameOnRight
})}
/>
</a>
{(pref.view === 'tree' || pref.view === 'network') && (
<Tooltip
content={AppUtils.userMsgsList[showToolTip?.msgKey] || 'Group Nodes'}
visible={pref.groupNodes && showToolTip !== undefined && !showToolTip?.display}
duration={showToolTip?.duration}
zIndex={1}>
<a
className={`group-nodes-button group-nodes-button${!pref.groupNodes ? '' : '-on'}`}
title={pref.view === 'tree' ? 'Group Nodes' : 'Collapse Pods'}
onClick={() => this.toggleCompactView(application.metadata.name, pref)}>
<i className={classNames('fa fa-object-group fa-fw')} />
</a>
</Tooltip>
)}
<span className={`separator`} />
<a className={`group-nodes-button`} onClick={() => expandAll()} title='Expand all child nodes of all parent nodes'>
<i className='fa fa-plus fa-fw' />
</a>
<a className={`group-nodes-button`} onClick={() => collapseAll()} title='Collapse all child nodes of all parent nodes'>
<i className='fa fa-minus fa-fw' />
</a>
<span className={`separator`} />
<span>
<a className={`group-nodes-button`} onClick={() => setZoom(0.1)} title='Zoom in'>
<i className='fa fa-search-plus fa-fw' />
</a>
<a className={`group-nodes-button`} onClick={() => setZoom(-0.1)} title='Zoom out'>
<i className='fa fa-search-minus fa-fw' />
</a>
<div className={`zoom-value`}>{zoomNum}%</div>
</span>
</div>
<ApplicationResourceTree
nodeFilter={node => this.filterTreeNode(node, treeFilter)}
selectedNodeFullName={this.selectedNodeKey}
onNodeClick={fullName => this.selectNode(fullName)}
nodeMenu={node =>
AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
this.getApplicationActionMenu(application, false)
)
}
showCompactNodes={pref.groupNodes}
userMsgs={pref.userHelpTipMsgs}
tree={tree}
app={application}
showOrphanedResources={pref.orphanedResources}
useNetworkingHierarchy={pref.view === 'network'}
onClearFilter={clearFilter}
onGroupdNodeClick={groupdedNodeIds => openGroupNodeDetails(groupdedNodeIds)}
zoom={pref.zoom}
podGroupCount={pref.podGroupCount}
appContext={this.appContext}
nameDirection={this.state.truncateNameOnRight}
filters={pref.resourceFilter}
setTreeFilterGraph={setFilterGraph}
updateUsrHelpTipMsgs={updateHelpTipState}
setShowCompactNodes={setShowCompactNodes}
setNodeExpansion={(node, isExpanded) => this.setNodeExpansion(node, isExpanded)}
getNodeExpansion={node => this.getNodeExpansion(node)}
/>
</>
)) ||
(pref.view === 'pods' && (
<PodView
tree={tree}
app={application}
onItemClick={fullName => this.selectNode(fullName)}
nodeMenu={node =>
AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
this.getApplicationActionMenu(application, false)
)
}
quickStarts={node => AppUtils.renderResourceButtons(node, application, tree, this.appContext.apis, this.appChanged)}
/>
)) ||
(this.state.extensionsMap[pref.view] != null && (
<ExtensionView extension={this.state.extensionsMap[pref.view]} application={application} tree={tree} />
)) || (
<div>
<div className='application-details__wrapper'>
<div className='application-details__status-panel'>
<ApplicationStatusPanel
application={application}
showDiff={() => this.selectNode(appFullName, 0, 'diff')}
showOperation={() => this.setOperationStatusVisible(true)}
showConditions={() => this.setConditionsStatusVisible(true)}
showMetadataInfo={revision => this.setState({...this.state, revision})}
/>
</div>
<div className='application-details__tree'>
{refreshing && <p className='application-details__refreshing-label'>Refreshing</p>}
{((pref.view === 'tree' || pref.view === 'network') && (
<>
<DataLoader load={() => services.viewPreferences.getPreferences()}>
{viewPref => (
<ApplicationDetailsFilters
@@ -543,42 +438,149 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
onSetFilter={setFilter}
onClearFilter={clearFilter}
collapsed={viewPref.hideSidebar}
resourceNodes={filteredRes}
resourceNodes={this.state.filteredGraph}
/>
)}
</DataLoader>
{(filteredRes.length > 0 && (
<Paginate
page={this.state.page}
data={filteredRes}
onPageChange={page => this.setState({page})}
preferencesKey='application-details'>
{data => (
<ApplicationResourceList
onNodeClick={fullName => this.selectNode(fullName)}
resources={data}
nodeMenu={node =>
AppUtils.renderResourceMenu(
{...node, root: node},
application,
tree,
this.appContext.apis,
this.appChanged,
() => this.getApplicationActionMenu(application, false)
)
}
<div className='graph-options-panel'>
<a
className={`group-nodes-button`}
onClick={() => {
toggleNameDirection();
}}
title={this.state.truncateNameOnRight ? 'Truncate resource name right' : 'Truncate resource name left'}>
<i
className={classNames({
'fa fa-align-right': this.state.truncateNameOnRight,
'fa fa-align-left': !this.state.truncateNameOnRight
})}
/>
</a>
{(pref.view === 'tree' || pref.view === 'network') && (
<Tooltip
content={AppUtils.userMsgsList[showToolTip?.msgKey] || 'Group Nodes'}
visible={pref.groupNodes && showToolTip !== undefined && !showToolTip?.display}
duration={showToolTip?.duration}
zIndex={1}>
<a
className={`group-nodes-button group-nodes-button${!pref.groupNodes ? '' : '-on'}`}
title={pref.view === 'tree' ? 'Group Nodes' : 'Collapse Pods'}
onClick={() => this.toggleCompactView(application.metadata.name, pref)}>
<i className={classNames('fa fa-object-group fa-fw')} />
</a>
</Tooltip>
)}
<span className={`separator`} />
<a className={`group-nodes-button`} onClick={() => expandAll()} title='Expand all child nodes of all parent nodes'>
<i className='fa fa-plus fa-fw' />
</a>
<a className={`group-nodes-button`} onClick={() => collapseAll()} title='Collapse all child nodes of all parent nodes'>
<i className='fa fa-minus fa-fw' />
</a>
<span className={`separator`} />
<span>
<a className={`group-nodes-button`} onClick={() => setZoom(0.1)} title='Zoom in'>
<i className='fa fa-search-plus fa-fw' />
</a>
<a className={`group-nodes-button`} onClick={() => setZoom(-0.1)} title='Zoom out'>
<i className='fa fa-search-minus fa-fw' />
</a>
<div className={`zoom-value`}>{zoomNum}%</div>
</span>
</div>
<ApplicationResourceTree
nodeFilter={node => this.filterTreeNode(node, treeFilter)}
selectedNodeFullName={this.selectedNodeKey}
onNodeClick={fullName => this.selectNode(fullName)}
nodeMenu={node =>
AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
this.getApplicationActionMenu(application, false)
)
}
showCompactNodes={pref.groupNodes}
userMsgs={pref.userHelpTipMsgs}
tree={tree}
app={application}
showOrphanedResources={pref.orphanedResources}
useNetworkingHierarchy={pref.view === 'network'}
onClearFilter={clearFilter}
onGroupdNodeClick={groupdedNodeIds => openGroupNodeDetails(groupdedNodeIds)}
zoom={pref.zoom}
podGroupCount={pref.podGroupCount}
appContext={this.appContext}
nameDirection={this.state.truncateNameOnRight}
filters={pref.resourceFilter}
setTreeFilterGraph={setFilterGraph}
updateUsrHelpTipMsgs={updateHelpTipState}
setShowCompactNodes={setShowCompactNodes}
setNodeExpansion={(node, isExpanded) => this.setNodeExpansion(node, isExpanded)}
getNodeExpansion={node => this.getNodeExpansion(node)}
/>
</>
)) ||
(pref.view === 'pods' && (
<PodView
tree={tree}
app={application}
onItemClick={fullName => this.selectNode(fullName)}
nodeMenu={node =>
AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
this.getApplicationActionMenu(application, false)
)
}
quickStarts={node => AppUtils.renderResourceButtons(node, application, tree, this.appContext.apis, this.appChanged)}
/>
)) ||
(this.state.extensionsMap[pref.view] != null && (
<ExtensionView extension={this.state.extensionsMap[pref.view]} application={application} tree={tree} />
)) || (
<div>
<DataLoader load={() => services.viewPreferences.getPreferences()}>
{viewPref => (
<ApplicationDetailsFilters
pref={pref}
tree={tree}
onSetFilter={setFilter}
onClearFilter={clearFilter}
collapsed={viewPref.hideSidebar}
resourceNodes={filteredRes}
/>
)}
</Paginate>
)) || (
<EmptyState icon='fa fa-search'>
<h4>No resources found</h4>
<h5>Try to change filter criteria</h5>
</EmptyState>
)}
</div>
)}
</DataLoader>
{(filteredRes.length > 0 && (
<Paginate
page={this.state.page}
data={filteredRes}
onPageChange={page => this.setState({page})}
preferencesKey='application-details'>
{data => (
<ApplicationResourceList
onNodeClick={fullName => this.selectNode(fullName)}
resources={data}
nodeMenu={node =>
AppUtils.renderResourceMenu(
{...node, root: node},
application,
tree,
this.appContext.apis,
this.appChanged,
() => this.getApplicationActionMenu(application, false)
)
}
tree={tree}
/>
)}
</Paginate>
)) || (
<EmptyState icon='fa fa-search'>
<h4>No resources found</h4>
<h5>Try to change filter criteria</h5>
</EmptyState>
)}
</div>
)}
</div>
</div>
<SlidingPanel isShown={this.state.groupedResources.length > 0} onClose={() => this.closeGroupedNodesPanel()}>
<div className='application-details__sliding-panel-pagination-wrap'>
@@ -748,12 +750,12 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
return [
{
iconClassName: 'fa fa-info-circle',
title: <ActionMenuItem actionLabel='App Details' />,
title: <ActionMenuItem actionLabel='Details' />,
action: () => this.selectNode(fullName)
},
{
iconClassName: 'fa fa-file-medical',
title: <ActionMenuItem actionLabel='App Diff' />,
title: <ActionMenuItem actionLabel='Diff' />,
action: () => this.selectNode(fullName, 0, 'diff'),
disabled: app.status.sync.status === appModels.SyncStatuses.Synced
},

View File

@@ -14,14 +14,21 @@ export interface LayoutProps {
const getBGColor = (theme: string): string => (theme === 'light' ? '#dee6eb' : '#100f0f');
export const Layout = (props: LayoutProps) => (
<div className={props.pref.theme ? 'theme-' + props.pref.theme : 'theme-light'}>
<div className={`cd-layout ${props.isExtension ? 'cd-layout--extension' : ''}`}>
<Sidebar onVersionClick={props.onVersionClick} navItems={props.navItems} pref={props.pref} />
{props.pref.theme ? (document.body.style.background = getBGColor(props.pref.theme)) : null}
<div className={`cd-layout__content ${props.pref.hideSidebar ? 'cd-layout__content--sb-collapsed' : 'cd-layout__content--sb-expanded'} custom-styles`}>
{props.children}
export const Layout = (props: LayoutProps) => {
React.useEffect(() => {
if (props.pref.theme) {
document.body.style.background = getBGColor(props.pref.theme);
}
}, [props.pref.theme]);
return (
<div className={props.pref.theme ? 'theme-' + props.pref.theme : 'theme-light'}>
<div className={`cd-layout ${props.isExtension ? 'cd-layout--extension' : ''}`}>
<Sidebar onVersionClick={props.onVersionClick} navItems={props.navItems} pref={props.pref} />
<div className={`cd-layout__content ${props.pref.hideSidebar ? 'cd-layout__content--sb-collapsed' : 'cd-layout__content--sb-expanded'} custom-styles`}>
{props.children}
</div>
</div>
</div>
</div>
);
);
};

View File

@@ -75,10 +75,10 @@
}
.sb-page-wrapper {
padding-left: $sidebar-width - 60px;
padding-left: $sidebar-width;
&__sidebar-collapsed {
padding-left: $collapsed-sidebar-width - 60px;
padding-left: $collapsed-sidebar-width;
.flex-top-bar {
left: $collapsed-sidebar-width;
}

View File

@@ -855,7 +855,9 @@ func NormalizeApplicationSpec(spec *argoappv1.ApplicationSpec) *argoappv1.Applic
if spec.Project == "" {
spec.Project = argoappv1.DefaultAppProjectName
}
if spec.SyncPolicy.IsZero() {
spec.SyncPolicy = nil
}
if spec.Sources != nil && len(spec.Sources) > 0 {
for _, source := range spec.Sources {
NormalizeSource(&source)

65
util/cache/mocks/cacheclient.go vendored Normal file
View File

@@ -0,0 +1,65 @@
package mocks
import (
"context"
"time"
cache "github.com/argoproj/argo-cd/v2/util/cache"
"github.com/stretchr/testify/mock"
)
type MockCacheClient struct {
mock.Mock
BaseCache cache.CacheClient
ReadDelay time.Duration
WriteDelay time.Duration
}
func (c *MockCacheClient) Set(item *cache.Item) error {
args := c.Called(item)
if len(args) > 0 && args.Get(0) != nil {
return args.Get(0).(error)
}
if c.WriteDelay > 0 {
time.Sleep(c.WriteDelay)
}
return c.BaseCache.Set(item)
}
func (c *MockCacheClient) Get(key string, obj interface{}) error {
args := c.Called(key, obj)
if len(args) > 0 && args.Get(0) != nil {
return args.Get(0).(error)
}
if c.ReadDelay > 0 {
time.Sleep(c.ReadDelay)
}
return c.BaseCache.Get(key, obj)
}
func (c *MockCacheClient) Delete(key string) error {
args := c.Called(key)
if len(args) > 0 && args.Get(0) != nil {
return args.Get(0).(error)
}
if c.WriteDelay > 0 {
time.Sleep(c.WriteDelay)
}
return c.BaseCache.Delete(key)
}
func (c *MockCacheClient) OnUpdated(ctx context.Context, key string, callback func() error) error {
args := c.Called(ctx, key, callback)
if len(args) > 0 && args.Get(0) != nil {
return args.Get(0).(error)
}
return c.BaseCache.OnUpdated(ctx, key, callback)
}
func (c *MockCacheClient) NotifyUpdated(key string) error {
args := c.Called(key)
if len(args) > 0 && args.Get(0) != nil {
return args.Get(0).(error)
}
return c.BaseCache.NotifyUpdated(key)
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/argoproj/argo-cd/v2/common"
"github.com/sirupsen/logrus"
"golang.org/x/net/proxy"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
@@ -63,7 +64,7 @@ func BlockingDial(ctx context.Context, network, address string, creds credential
dialer := func(ctx context.Context, address string) (net.Conn, error) {
conn, err := (&net.Dialer{Cancel: ctx.Done()}).Dial(network, address)
conn, err := proxy.Dial(ctx, network, address)
if err != nil {
writeResult(err)
return nil, err

View File

@@ -19,7 +19,7 @@ const maxCookieLength = 4093
// max number of chunks a cookie can be broken into. To be compatible with
// widest range of browsers, you shouldn't create more than 30 cookies per domain
var maxCookieNumber = env.ParseNumFromEnv(common.EnvMaxCookieNumber, 20, 0, math.MaxInt64)
var maxCookieNumber = env.ParseNumFromEnv(common.EnvMaxCookieNumber, 20, 0, math.MaxInt)
// MakeCookieMetadata generates a string representing a Web cookie. Yum!
func MakeCookieMetadata(key, value string, flags ...string) ([]string, error) {