Compare commits

...

17 Commits

Author SHA1 Message Date
github-actions[bot]
0704aa6506 Bump version to 2.12.0-rc4 (#19060)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: crenshaw-dev <350466+crenshaw-dev@users.noreply.github.com>
2024-07-15 13:28:20 -04:00
Alexandre Gaudreault
511d6e371c chore: bump gitops-engine (#19056)
Signed-off-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
2024-07-15 12:08:15 -04:00
gcp-cherry-pick-bot[bot]
065f8494cf fix(cli): Get Redis password from secret in loadClusters() (#18951) (#18955)
* Get Redis password from secret in `loadClusters()`



* feat: support redis password in admin stats command



* Simplify code



---------

Signed-off-by: David Wu <155603967+david-wu-octopus@users.noreply.github.com>
Signed-off-by: pashakostohrys <pavel@codefresh.io>
Co-authored-by: david-wu-octopus <155603967+david-wu-octopus@users.noreply.github.com>
Co-authored-by: pashakostohrys <pavel@codefresh.io>
2024-07-05 11:12:24 -04:00
gcp-cherry-pick-bot[bot]
beacacc43d fix(appset): missing permissions (#18829) (#18943) (#18944) 2024-07-04 10:44:58 -04:00
gcp-cherry-pick-bot[bot]
81d454215d remove unwanted updating of source-position in app set command (#18887) (#18897)
Signed-off-by: ishitasequeira <ishiseq29@gmail.com>
Co-authored-by: Ishita Sequeira <46771830+ishitasequeira@users.noreply.github.com>
2024-07-02 13:14:21 -04:00
github-actions[bot]
1237d4ea17 Bump version to 2.12.0-rc3 (#18893)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: crenshaw-dev <350466+crenshaw-dev@users.noreply.github.com>
2024-07-02 13:04:14 -04:00
Michael Crenshaw
b211d3e038 chore: bump gitops-engine (#18871)
Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2024-07-01 13:22:54 -04:00
gcp-cherry-pick-bot[bot]
50c32b53f0 docs: Fix .path to .path.segments go template (#18872) (#18873)
Signed-off-by: Jaeseok Lee <devsunb@gmail.com>
Co-authored-by: Jaeseok Lee <devsunb@gmail.com>
2024-07-01 10:52:36 -04:00
gcp-cherry-pick-bot[bot]
444d332d0a fix: Handle nil health check in post-delete hooks (#18270) (#18767) (#18828)
* fix: Handle nil health check in post-delete hooks



* test: Validate deletion of RBAC resources for post-delete hook



* Update appcontroller_test.go



---------

Signed-off-by: Yonatan Sasson <yonatanxd7@gmail.com>
Signed-off-by: Yonatan Sasson <107778824+Yuni-sa@users.noreply.github.com>
Co-authored-by: Yonatan Sasson <107778824+Yuni-sa@users.noreply.github.com>
2024-06-26 17:21:19 +03:00
gcp-cherry-pick-bot[bot]
573c771d2b fix(webhook): bitbucket and azure not triggering refresh (#18289) (#18765) (#18818)
* fix(webhook): bitbucket and azure webhook not triggering refresh



* update unit test



* fix merge



* adjust logic for reposerver using ls-remote



---------

Signed-off-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
Co-authored-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
2024-06-26 08:48:16 -04:00
Michael Crenshaw
e616dff2d1 fix(appset): revert "keep reconciling even when params error occurred" (#17062) (#18781)
This reverts commit 86369ca71d.

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2024-06-25 15:33:59 -04:00
gcp-cherry-pick-bot[bot]
e36e19de4e chore(deps): bump github.com/hashicorp/go-retryablehttp (#18805) (#18811)
Bumps [github.com/hashicorp/go-retryablehttp](https://github.com/hashicorp/go-retryablehttp) from 0.7.4 to 0.7.7.
- [Changelog](https://github.com/hashicorp/go-retryablehttp/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/go-retryablehttp/compare/v0.7.4...v0.7.7)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/go-retryablehttp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-25 11:29:12 -04:00
github-actions[bot]
bbfa79ad9e Bump version to 2.12.0-rc2 (#18802)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: crenshaw-dev <350466+crenshaw-dev@users.noreply.github.com>
2024-06-24 16:31:31 -04:00
gcp-cherry-pick-bot[bot]
00fc93b8b2 fix: Bug in edit support in Sources tab; Input to loader (#17588) (#18800) (#18801)
Co-authored-by: Keith Chong <kykchong@redhat.com>
2024-06-24 14:02:06 -04:00
gcp-cherry-pick-bot[bot]
16c20a84b8 fix(server): could not find source for metadata revision (#18744) (#18763) (#18782)
* fix(server): could not find source for metadata revision (#18744)



* lint



* the linter behaves poorly



* fix test



* share logic with chart endpoint



* more intuitive check



* remove debug line



---------

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2024-06-24 09:55:56 -04:00
gcp-cherry-pick-bot[bot]
2a9a62eeb7 fix(ui): set project to empty string if undefined (#18732) (#18733)
There are some situations where the project will be `undefined`. When
that happens, attempting to delete a repo won't be possible, since
the backend will be looking for a project with the literal name
`undefined`. To fix this, set an empty string for `undefined|null`
values.

Signed-off-by: Blake Pettersson <blake.pettersson@gmail.com>
Co-authored-by: Blake Pettersson <blake.pettersson@gmail.com>
2024-06-20 12:00:39 +03:00
github-actions[bot]
fe60670885 Bump version to 2.12.0-rc1 (#18716)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ishitasequeira <46771830+ishitasequeira@users.noreply.github.com>
2024-06-18 09:09:23 -04:00
32 changed files with 642 additions and 392 deletions

View File

@@ -1 +1 @@
2.12.0
2.12.0-rc4

View File

@@ -129,20 +129,18 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
// Log a warning if there are unrecognized generators
_ = utils.CheckInvalidGenerators(&applicationSetInfo)
// desiredApplications is the main list of all expected Applications from all generators in this appset.
desiredApplications, applicationSetReason, generatorsErr := r.generateApplications(logCtx, applicationSetInfo)
if generatorsErr != nil {
desiredApplications, applicationSetReason, err := r.generateApplications(logCtx, applicationSetInfo)
if err != nil {
_ = r.setApplicationSetStatusCondition(ctx,
&applicationSetInfo,
argov1alpha1.ApplicationSetCondition{
Type: argov1alpha1.ApplicationSetConditionErrorOccurred,
Message: generatorsErr.Error(),
Message: err.Error(),
Reason: string(applicationSetReason),
Status: argov1alpha1.ApplicationSetConditionStatusTrue,
}, parametersGenerated,
)
if len(desiredApplications) < 1 {
return ctrl.Result{}, generatorsErr
}
return ctrl.Result{}, err
}
parametersGenerated = true
@@ -320,7 +318,7 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
requeueAfter := r.getMinRequeueAfter(&applicationSetInfo)
if len(validateErrors) == 0 && generatorsErr == nil {
if len(validateErrors) == 0 {
if err := r.setApplicationSetStatusCondition(ctx,
&applicationSetInfo,
argov1alpha1.ApplicationSetCondition{

View File

@@ -2471,90 +2471,6 @@ func TestReconcilerValidationProjectErrorBehaviour(t *testing.T) {
require.Error(t, err)
}
func TestReconcilerCreateAppsRecoveringRenderError(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
err = v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
project := v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "argocd"},
}
appSet := v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "argocd",
},
Spec: v1alpha1.ApplicationSetSpec{
GoTemplate: true,
Generators: []v1alpha1.ApplicationSetGenerator{
{
List: &v1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{{
Raw: []byte(`{"name": "very-good-app"}`),
}, {
Raw: []byte(`{"name": "bad-app"}`),
}},
},
},
},
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{
Name: "{{ index (splitList \"-\" .name ) 2 }}",
Namespace: "argocd",
},
Spec: v1alpha1.ApplicationSpec{
Source: &v1alpha1.ApplicationSource{RepoURL: "https://github.com/argoproj/argocd-example-apps", Path: "guestbook"},
Project: "default",
Destination: v1alpha1.ApplicationDestination{Server: "https://kubernetes.default.svc"},
},
},
},
}
kubeclientset := kubefake.NewSimpleClientset()
argoDBMock := dbmocks.ArgoDB{}
argoObjs := []runtime.Object{&project}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appSet).WithStatusSubresource(&appSet).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
r := ApplicationSetReconciler{
Client: client,
Scheme: scheme,
Renderer: &utils.Render{},
Recorder: record.NewFakeRecorder(1),
Cache: &fakeCache{},
Generators: map[string]generators.Generator{
"List": generators.NewListGenerator(),
},
ArgoDB: &argoDBMock,
ArgoAppClientset: appclientset.NewSimpleClientset(argoObjs...),
KubeClientset: kubeclientset,
Policy: v1alpha1.ApplicationsSyncPolicySync,
ArgoCDNamespace: "argocd",
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "argocd",
Name: "name",
},
}
// Verify that on generatorsError, no error is returned, but the object is requeued
res, err := r.Reconcile(context.Background(), req)
require.NoError(t, err)
assert.Equal(t, ReconcileRequeueOnValidationError, res.RequeueAfter)
var app v1alpha1.Application
// make sure good app got created
err = r.Client.Get(context.TODO(), crtclient.ObjectKey{Namespace: "argocd", Name: "app"}, &app)
require.NoError(t, err)
assert.Equal(t, "app", app.Name)
}
func TestSetApplicationSetStatusCondition(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)

View File

@@ -104,7 +104,17 @@ func loadClusters(ctx context.Context, kubeClient *kubernetes.Clientset, appClie
if err != nil {
return nil, err
}
client := redis.NewClient(&redis.Options{Addr: fmt.Sprintf("localhost:%d", port)})
redisOptions := &redis.Options{Addr: fmt.Sprintf("localhost:%d", port)}
secret, err := kubeClient.CoreV1().Secrets(namespace).Get(context.Background(), defaulRedisInitialPasswordSecretName, v1.GetOptions{})
if err == nil {
if _, ok := secret.Data[defaultResisInitialPasswordKey]; ok {
redisOptions.Password = string(secret.Data[defaultResisInitialPasswordKey])
}
}
client := redis.NewClient(redisOptions)
compressionType, err := cacheutil.CompressionTypeFromString(redisCompressionStr)
if err != nil {
return nil, err

View File

@@ -776,8 +776,6 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
}
}
// sourcePosition startes with 1, thus, it needs to be decreased by 1 to find the correct index in the list of sources
sourcePosition = sourcePosition - 1
visited := cmdutil.SetAppSpecOptions(c.Flags(), &app.Spec, &appOpts, sourcePosition)
if visited == 0 {
log.Error("Please set at least one option to update")

View File

@@ -374,8 +374,8 @@ data:
var fakePostDeleteHook = `
{
"apiVersion": "v1",
"kind": "Pod",
"apiVersion": "batch/v1",
"kind": "Job",
"metadata": {
"name": "post-delete-hook",
"namespace": "default",
@@ -388,22 +388,93 @@ var fakePostDeleteHook = `
}
},
"spec": {
"containers": [
{
"name": "post-delete-hook",
"image": "busybox",
"restartPolicy": "Never",
"command": [
"/bin/sh",
"-c",
"sleep 5 && echo hello from the post-delete-hook pod"
]
"template": {
"metadata": {
"name": "post-delete-hook"
},
"spec": {
"containers": [
{
"name": "post-delete-hook",
"image": "busybox",
"command": [
"/bin/sh",
"-c",
"sleep 5 && echo hello from the post-delete-hook job"
]
}
],
"restartPolicy": "Never"
}
]
}
}
}
`
var fakeServiceAccount = `
{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"name": "hook-serviceaccount",
"namespace": "default",
"annotations": {
"argocd.argoproj.io/hook": "PostDelete",
"argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded"
}
}
}
`
var fakeRole = `
{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "Role",
"metadata": {
"name": "hook-role",
"namespace": "default",
"annotations": {
"argocd.argoproj.io/hook": "PostDelete",
"argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded"
}
},
"rules": [
{
"apiGroups": [""],
"resources": ["secrets"],
"verbs": ["get", "delete", "list"]
}
]
}
`
var fakeRoleBinding = `
{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "RoleBinding",
"metadata": {
"name": "hook-rolebinding",
"namespace": "default",
"annotations": {
"argocd.argoproj.io/hook": "PostDelete",
"argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded"
}
},
"roleRef": {
"apiGroup": "rbac.authorization.k8s.io",
"kind": "Role",
"name": "hook-role"
},
"subjects": [
{
"kind": "ServiceAccount",
"name": "hook-serviceaccount",
"namespace": "default"
}
]
}
`
func newFakeApp() *v1alpha1.Application {
return createFakeApp(fakeApp)
}
@@ -439,12 +510,39 @@ func newFakeCM() map[string]interface{} {
}
func newFakePostDeleteHook() map[string]interface{} {
var cm map[string]interface{}
err := yaml.Unmarshal([]byte(fakePostDeleteHook), &cm)
var hook map[string]interface{}
err := yaml.Unmarshal([]byte(fakePostDeleteHook), &hook)
if err != nil {
panic(err)
}
return cm
return hook
}
func newFakeRoleBinding() map[string]interface{} {
var roleBinding map[string]interface{}
err := yaml.Unmarshal([]byte(fakeRoleBinding), &roleBinding)
if err != nil {
panic(err)
}
return roleBinding
}
func newFakeRole() map[string]interface{} {
var role map[string]interface{}
err := yaml.Unmarshal([]byte(fakeRole), &role)
if err != nil {
panic(err)
}
return role
}
func newFakeServiceAccount() map[string]interface{} {
var serviceAccount map[string]interface{}
err := yaml.Unmarshal([]byte(fakeServiceAccount), &serviceAccount)
if err != nil {
panic(err)
}
return serviceAccount
}
func TestAutoSync(t *testing.T) {
@@ -721,7 +819,7 @@ func TestFinalizeAppDeletion(t *testing.T) {
// Ensure any stray resources irregularly labeled with instance label of app are not deleted upon deleting,
// when app project restriction is in place
t.Run("ProjectRestrictionEnforced", func(*testing.T) {
t.Run("ProjectRestrictionEnforced", func(t *testing.T) {
restrictedProj := v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "restricted",
@@ -882,7 +980,13 @@ func TestFinalizeAppDeletion(t *testing.T) {
app.SetPostDeleteFinalizer()
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
liveHook := &unstructured.Unstructured{Object: newFakePostDeleteHook()}
require.NoError(t, unstructured.SetNestedField(liveHook.Object, "Succeeded", "status", "phase"))
conditions := []interface{}{
map[string]interface{}{
"type": "Complete",
"status": "True",
},
}
require.NoError(t, unstructured.SetNestedField(liveHook.Object, conditions, "status", "conditions"))
ctrl := newFakeController(&fakeData{
manifestResponses: []*apiclient.ManifestResponse{{
Manifests: []string{fakePostDeleteHook},
@@ -916,15 +1020,27 @@ func TestFinalizeAppDeletion(t *testing.T) {
app := newFakeApp()
app.SetPostDeleteFinalizer("cleanup")
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
liveRoleBinding := &unstructured.Unstructured{Object: newFakeRoleBinding()}
liveRole := &unstructured.Unstructured{Object: newFakeRole()}
liveServiceAccount := &unstructured.Unstructured{Object: newFakeServiceAccount()}
liveHook := &unstructured.Unstructured{Object: newFakePostDeleteHook()}
require.NoError(t, unstructured.SetNestedField(liveHook.Object, "Succeeded", "status", "phase"))
conditions := []interface{}{
map[string]interface{}{
"type": "Complete",
"status": "True",
},
}
require.NoError(t, unstructured.SetNestedField(liveHook.Object, conditions, "status", "conditions"))
ctrl := newFakeController(&fakeData{
manifestResponses: []*apiclient.ManifestResponse{{
Manifests: []string{fakePostDeleteHook},
Manifests: []string{fakeRoleBinding, fakeRole, fakeServiceAccount, fakePostDeleteHook},
}},
apps: []runtime.Object{app, &defaultProj},
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
kube.GetResourceKey(liveHook): liveHook,
kube.GetResourceKey(liveRoleBinding): liveRoleBinding,
kube.GetResourceKey(liveRole): liveRole,
kube.GetResourceKey(liveServiceAccount): liveServiceAccount,
kube.GetResourceKey(liveHook): liveHook,
},
}, nil)
@@ -943,9 +1059,14 @@ func TestFinalizeAppDeletion(t *testing.T) {
return []*v1alpha1.Cluster{}, nil
})
require.NoError(t, err)
// post-delete hook is deleted
require.Len(t, ctrl.kubectl.(*MockKubectl).DeletedResources, 1)
require.Equal(t, "post-delete-hook", ctrl.kubectl.(*MockKubectl).DeletedResources[0].Name)
// post-delete hooks are deleted
require.Len(t, ctrl.kubectl.(*MockKubectl).DeletedResources, 4)
deletedResources := []string{}
for _, res := range ctrl.kubectl.(*MockKubectl).DeletedResources {
deletedResources = append(deletedResources, res.Name)
}
expectedNames := []string{"hook-rolebinding", "hook-role", "hook-serviceaccount", "post-delete-hook"}
require.ElementsMatch(t, expectedNames, deletedResources, "Deleted resources should match expected names")
// finalizer is not removed
assert.False(t, patched)
})

View File

@@ -98,6 +98,18 @@ func (ctrl *ApplicationController) executePostDeleteHooks(app *v1alpha1.Applicat
if err != nil {
return false, err
}
if hookHealth == nil {
logCtx.WithFields(log.Fields{
"group": obj.GroupVersionKind().Group,
"version": obj.GroupVersionKind().Version,
"kind": obj.GetKind(),
"name": obj.GetName(),
"namespace": obj.GetNamespace(),
}).Info("No health check defined for resource, considering it healthy")
hookHealth = &health.HealthStatus{
Status: health.HealthStatusHealthy,
}
}
if hookHealth.Status == health.HealthStatusProgressing {
progressingHooksCnt++
}
@@ -128,6 +140,11 @@ func (ctrl *ApplicationController) cleanupPostDeleteHooks(liveObjs map[kube.Reso
if err != nil {
return false, err
}
if hookHealth == nil {
hookHealth = &health.HealthStatus{
Status: health.HealthStatusHealthy,
}
}
if health.IsWorse(aggregatedHealth, hookHealth.Status) {
aggregatedHealth = hookHealth.Status
}

View File

@@ -326,7 +326,7 @@ As with other generators, clusters *must* already be defined within Argo CD, in
In addition to the flattened key/value pairs from the configuration file, the following generator parameters are provided:
- `{{.path.path}}`: The path to the directory containing matching configuration file within the Git repository. Example: `/clusters/clusterA`, if the config file was `/clusters/clusterA/config.json`
- `{{index .path n}}`: The path to the matching configuration file within the Git repository, split into array elements (`n` - array index). Example: `index .path 0: clusters`, `index .path 1: clusterA`
- `{{index .path.segments n}}`: The path to the matching configuration file within the Git repository, split into array elements (`n` - array index). Example: `index .path.segments 0: clusters`, `index .path.segments 1: clusterA`
- `{{.path.basename}}`: Basename of the path to the directory containing the configuration file (e.g. `clusterA`, with the above example.)
- `{{.path.basenameNormalized}}`: This field is the same as `.path.basename` with unsupported characters replaced with `-` (e.g. a `path` of `/directory/directory_2`, and `.path.basename` of `directory_2` would produce `directory-2` here).
- `{{.path.filename}}`: The matched filename. e.g., `config.json` in the above example.
@@ -360,7 +360,7 @@ spec:
files:
- path: "applicationset/examples/git-generator-files-discovery/cluster-config/**/config.json"
values:
base_dir: "{{index .path 0}}/{{index .path 1}}/{{index .path 2}}"
base_dir: "{{index .path.segments 0}}/{{index .path.segments 1}}/{{index .path.segments 2}}"
template:
metadata:
name: '{{.cluster.name}}-guestbook'

View File

@@ -1,6 +1,5 @@
| Argo CD version | Kubernetes versions |
|-----------------|---------------------|
| 2.7 | v1.26, v1.25, v1.24, v1.23 |
| 2.6 | v1.24, v1.23, v1.22 |
| 2.5 | v1.24, v1.23, v1.22 |
| 2.12 | |
| 2.11 | v1.29, v1.28, v1.27, v1.26, v1.25 |
| 2.10 | v1.28, v1.27, v1.26, v1.25 |

6
go.mod
View File

@@ -11,7 +11,7 @@ require (
github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d
github.com/alicebob/miniredis/v2 v2.30.4
github.com/antonmedv/expr v1.15.2
github.com/argoproj/gitops-engine v0.7.1-0.20240615185936-83ce6ca8cedc
github.com/argoproj/gitops-engine v0.7.1-0.20240714153147-adb68bcaab73
github.com/argoproj/notifications-engine v0.4.1-0.20240606074338-0802cd427621
github.com/argoproj/pkg v0.13.7-0.20230626144333-d56162821bd1
github.com/aws/aws-sdk-go v1.50.8
@@ -52,14 +52,14 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/grpc-ecosystem/grpc-gateway v1.16.0
github.com/hashicorp/go-retryablehttp v0.7.4
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/imdario/mergo v0.3.16
github.com/improbable-eng/grpc-web v0.15.0
github.com/itchyny/gojq v0.12.13
github.com/jeremywohl/flatten v1.0.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/ktrysmt/go-bitbucket v0.9.67
github.com/mattn/go-isatty v0.0.19
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-zglob v0.0.4
github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1

20
go.sum
View File

@@ -695,8 +695,8 @@ github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
github.com/appscode/go v0.0.0-20191119085241-0887d8ec2ecc/go.mod h1:OawnOmAL4ZX3YaPdN+8HTNwBveT1jMsqP74moa9XUbE=
github.com/argoproj/gitops-engine v0.7.1-0.20240615185936-83ce6ca8cedc h1:J7LJp2Gh9A9/eQN7Lg74JW+YOVO5NEjq5/cudGAiOwk=
github.com/argoproj/gitops-engine v0.7.1-0.20240615185936-83ce6ca8cedc/go.mod h1:ByLmH5B1Gs361tgI5x5f8oSFuBEXDYENYpG3zFDWtHU=
github.com/argoproj/gitops-engine v0.7.1-0.20240714153147-adb68bcaab73 h1:7kyTgFsPjvb6noafslp2pr7fBCS9s8OJ759LdLzrOro=
github.com/argoproj/gitops-engine v0.7.1-0.20240714153147-adb68bcaab73/go.mod h1:xMIbuLg9Qj2e0egTy+8NcukbhRaVmWwK9vm3aAQZoi4=
github.com/argoproj/notifications-engine v0.4.1-0.20240606074338-0802cd427621 h1:Yg1nt+D2uDK1SL2jSlfukA4yc7db184TTN7iWy3voRE=
github.com/argoproj/notifications-engine v0.4.1-0.20240606074338-0802cd427621/go.mod h1:N0A4sEws2soZjEpY4hgZpQS8mRIEw6otzwfkgc3g9uQ=
github.com/argoproj/pkg v0.13.7-0.20230626144333-d56162821bd1 h1:qsHwwOJ21K2Ao0xPju1sNuqphyMnMYkyB3ZLoLtxWpo=
@@ -892,6 +892,8 @@ github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+ne
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
@@ -1242,14 +1244,14 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-retryablehttp v0.5.1/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA=
github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
@@ -1383,13 +1385,15 @@ github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsI
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=

View File

@@ -27,6 +27,8 @@ rules:
- appprojects
verbs:
- get
- list
- watch
- apiGroups:
- argoproj.io
resources:
@@ -62,4 +64,4 @@ rules:
verbs:
- get
- list
- watch
- watch

View File

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

View File

@@ -20822,6 +20822,8 @@ rules:
- appprojects
verbs:
- get
- list
- watch
- apiGroups:
- argoproj.io
resources:
@@ -21268,7 +21270,7 @@ spec:
key: applicationsetcontroller.enable.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -21386,7 +21388,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -21639,7 +21641,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -21691,7 +21693,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -21963,7 +21965,7 @@ spec:
key: controller.ignore.normalizer.jq.timeout
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
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: latest
newTag: v2.12.0-rc4

View File

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

View File

@@ -20860,6 +20860,8 @@ rules:
- appprojects
verbs:
- get
- list
- watch
- apiGroups:
- argoproj.io
resources:
@@ -22609,7 +22611,7 @@ spec:
key: applicationsetcontroller.enable.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -22732,7 +22734,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -22814,7 +22816,7 @@ spec:
key: notificationscontroller.selfservice.enabled
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -22933,7 +22935,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -23214,7 +23216,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -23266,7 +23268,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -23590,7 +23592,7 @@ spec:
key: server.api.content.types
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -23889,7 +23891,7 @@ spec:
key: controller.ignore.normalizer.jq.timeout
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -149,6 +149,8 @@ rules:
- appprojects
verbs:
- get
- list
- watch
- apiGroups:
- argoproj.io
resources:
@@ -1686,7 +1688,7 @@ spec:
key: applicationsetcontroller.enable.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -1809,7 +1811,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -1891,7 +1893,7 @@ spec:
key: notificationscontroller.selfservice.enabled
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -2010,7 +2012,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -2291,7 +2293,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2343,7 +2345,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -2667,7 +2669,7 @@ spec:
key: server.api.content.types
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2966,7 +2968,7 @@ spec:
key: controller.ignore.normalizer.jq.timeout
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -20849,6 +20849,8 @@ rules:
- appprojects
verbs:
- get
- list
- watch
- apiGroups:
- argoproj.io
resources:
@@ -21726,7 +21728,7 @@ spec:
key: applicationsetcontroller.enable.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -21849,7 +21851,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -21931,7 +21933,7 @@ spec:
key: notificationscontroller.selfservice.enabled
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -22031,7 +22033,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -22284,7 +22286,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -22336,7 +22338,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -22658,7 +22660,7 @@ spec:
key: server.api.content.types
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -22957,7 +22959,7 @@ spec:
key: controller.ignore.normalizer.jq.timeout
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -138,6 +138,8 @@ rules:
- appprojects
verbs:
- get
- list
- watch
- apiGroups:
- argoproj.io
resources:
@@ -803,7 +805,7 @@ spec:
key: applicationsetcontroller.enable.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -926,7 +928,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -1008,7 +1010,7 @@ spec:
key: notificationscontroller.selfservice.enabled
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -1108,7 +1110,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -1361,7 +1363,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -1413,7 +1415,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -1735,7 +1737,7 @@ spec:
key: server.api.content.types
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2034,7 +2036,7 @@ spec:
key: controller.ignore.normalizer.jq.timeout
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc4
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -2769,7 +2769,10 @@ func (s *Service) UpdateRevisionForPaths(_ context.Context, request *apiclient.U
return nil, status.Errorf(codes.Internal, "unable to get changed files for repo %s with revision %s: %v", repo.Repo, revision, err)
}
changed := apppathutil.AppFilesHaveChanged(refreshPaths, files)
changed := false
if len(files) != 0 {
changed = apppathutil.AppFilesHaveChanged(refreshPaths, files)
}
if !changed {
logCtx.Debugf("no changes found for application %s in repo %s from revision %s to revision %s", request.AppName, repo.Repo, syncedRevision, revision)

View File

@@ -1495,71 +1495,9 @@ func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMe
return nil, err
}
var versionId int64 = 0
if q.VersionId != nil {
versionId = int64(*q.VersionId)
}
var source *v1alpha1.ApplicationSource
// To support changes between single source and multi source revisions
// we have to calculate if the operation has to be done as multisource or not.
// There are 2 different scenarios, checking current revision and historic revision
// - Current revision (VersionId is nil or 0):
// - The application is multi source and required version too -> multi source
// - The application is single source and the required version too -> single source
// - The application is multi source and the required version is single source -> single source
// - The application is single source and the required version is multi source -> multi source
// - Historic revision:
// - The application is multi source and the previous one too -> multi source
// - The application is single source and the previous one too -> single source
// - The application is multi source and the previous one is single source -> multi source
// - The application is single source and the previous one is multi source -> single source
isRevisionMultiSource := a.Spec.HasMultipleSources()
emptyHistory := len(a.Status.History) == 0
if !emptyHistory {
for _, h := range a.Status.History {
if h.ID == versionId {
isRevisionMultiSource = len(h.Revisions) > 0
break
}
}
}
// If the historical data is empty (because the app hasn't been synced yet)
// we can use the source, if not (the app has been synced at least once)
// we have to use the history because sources can be added/removed
if emptyHistory {
if isRevisionMultiSource {
source = &a.Spec.Sources[*q.SourceIndex]
} else {
s := a.Spec.GetSource()
source = &s
}
} else {
// the source count can change during the time, we cannot just trust in .status.sync
// because if a source has been added/removed, the revisions there won't match
// as this is only used for the UI and not internally, we can use the historical data
// using the specific revisionId
for _, h := range a.Status.History {
if h.ID == versionId {
// The iteration values are assigned to the respective iteration variables as in an assignment statement.
// The iteration variables may be declared by the “range” clause using a form of short variable declaration (:=).
// In this case their types are set to the types of the respective iteration values and their scope is the block of the "for" statement;
// they are re-used in each iteration. If the iteration variables are declared outside the "for" statement,
// after execution their values will be those of the last iteration.
// https://golang.org/ref/spec#For_statements
h := h
if isRevisionMultiSource {
source = &h.Sources[*q.SourceIndex]
} else {
source = &h.Source
}
}
}
}
if source == nil {
return nil, fmt.Errorf("revision not found: %w", err)
source, err := getAppSourceBySourceIndexAndVersionId(a, q.SourceIndex, q.VersionId)
if err != nil {
return nil, fmt.Errorf("error getting app source by source index and version ID: %w", err)
}
repo, err := s.db.GetRepository(ctx, source.RepoURL, proj.Name)
@@ -1585,22 +1523,9 @@ func (s *Server) RevisionChartDetails(ctx context.Context, q *application.Revisi
return nil, err
}
var source *v1alpha1.ApplicationSource
if a.Spec.HasMultipleSources() {
// the source count can change during the time, we cannot just trust in .status.sync
// because if a source has been added/removed, the revisions there won't match
// as this is only used for the UI and not internally, we can use the historical data
// using the specific revisionId
for _, h := range a.Status.History {
if h.ID == int64(*q.VersionId) {
source = &h.Sources[*q.SourceIndex]
}
}
if source == nil {
return nil, fmt.Errorf("revision not found: %w", err)
}
} else {
source = a.Spec.Source
source, err := getAppSourceBySourceIndexAndVersionId(a, q.SourceIndex, q.VersionId)
if err != nil {
return nil, fmt.Errorf("error getting app source by source index and version ID: %w", err)
}
if source.Chart == "" {
@@ -1622,6 +1547,76 @@ func (s *Server) RevisionChartDetails(ctx context.Context, q *application.Revisi
})
}
// getAppSourceBySourceIndexAndVersionId returns the source for a specific source index and version ID. Source index and
// version ID are optional. If the source index is not specified, it defaults to 0. If the version ID is not specified,
// we use the source(s) currently configured for the app. If the version ID is specified, we find the source for that
// version ID. If the version ID is not found, we return an error. If the source index is out of bounds for whichever
// source we choose (configured sources or sources for a specific version), we return an error.
func getAppSourceBySourceIndexAndVersionId(a *appv1.Application, sourceIndexMaybe *int32, versionIdMaybe *int32) (appv1.ApplicationSource, error) {
// Start with all the app's configured sources.
sources := a.Spec.GetSources()
// If the user specified a version, get the sources for that version. If the version is not found, return an error.
if versionIdMaybe != nil {
versionId := int64(*versionIdMaybe)
var err error
sources, err = getSourcesByVersionId(a, versionId)
if err != nil {
return appv1.ApplicationSource{}, fmt.Errorf("error getting source by version ID: %w", err)
}
}
// Start by assuming we want the first source.
sourceIndex := 0
// If the user specified a source index, use that instead.
if sourceIndexMaybe != nil {
sourceIndex = int(*sourceIndexMaybe)
if sourceIndex >= len(sources) {
if len(sources) == 1 {
return appv1.ApplicationSource{}, fmt.Errorf("source index %d not found because there is only 1 source", sourceIndex)
}
return appv1.ApplicationSource{}, fmt.Errorf("source index %d not found because there are only %d sources", sourceIndex, len(sources))
}
}
source := sources[sourceIndex]
return source, nil
}
// getRevisionHistoryByVersionId returns the revision history for a specific version ID.
// If the version ID is not found, it returns an empty revision history and false.
func getRevisionHistoryByVersionId(histories v1alpha1.RevisionHistories, versionId int64) (appv1.RevisionHistory, bool) {
for _, h := range histories {
if h.ID == versionId {
return h, true
}
}
return appv1.RevisionHistory{}, false
}
// getSourcesByVersionId returns the sources for a specific version ID. If there is no history, it returns an error.
// If the version ID is not found, it returns an error. If the version ID is found, and there are multiple sources,
// it returns the sources for that version ID. If the version ID is found, and there is only one source, it returns
// a slice with just the single source.
func getSourcesByVersionId(a *appv1.Application, versionId int64) ([]appv1.ApplicationSource, error) {
if len(a.Status.History) == 0 {
return nil, fmt.Errorf("version ID %d not found because the app has no history", versionId)
}
h, ok := getRevisionHistoryByVersionId(a.Status.History, versionId)
if !ok {
return nil, fmt.Errorf("revision history not found for version ID %d", versionId)
}
if len(h.Sources) > 0 {
return h.Sources, nil
}
return []v1alpha1.ApplicationSource{h.Source}, nil
}
func isMatchingResource(q *application.ResourcesQuery, key kube.ResourceKey) bool {
return (q.GetName() == "" || q.GetName() == key.Name) &&
(q.GetNamespace() == "" || q.GetNamespace() == key.Namespace) &&

View File

@@ -10,6 +10,8 @@ import (
"testing"
"time"
"k8s.io/utils/pointer"
"k8s.io/apimachinery/pkg/labels"
"github.com/argoproj/gitops-engine/pkg/health"
@@ -3025,3 +3027,265 @@ func TestServer_ResolveSourceRevisions_SingleSource(t *testing.T) {
assert.Equal(t, ([]string)(nil), sourceRevisions)
assert.Equal(t, ([]string)(nil), displayRevisions)
}
func Test_RevisionMetadata(t *testing.T) {
singleSourceApp := newTestApp()
singleSourceApp.Name = "single-source-app"
singleSourceApp.Spec = appv1.ApplicationSpec{
Source: &appv1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
Path: "helm-guestbook",
TargetRevision: "HEAD",
},
}
multiSourceApp := newTestApp()
multiSourceApp.Name = "multi-source-app"
multiSourceApp.Spec = appv1.ApplicationSpec{
Sources: []appv1.ApplicationSource{
{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
Path: "helm-guestbook",
TargetRevision: "HEAD",
},
{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
Path: "kustomize-guestbook",
TargetRevision: "HEAD",
},
},
}
singleSourceHistory := []appv1.RevisionHistory{
{
ID: 1,
Source: singleSourceApp.Spec.GetSource(),
Revision: "a",
},
}
multiSourceHistory := []appv1.RevisionHistory{
{
ID: 1,
Sources: multiSourceApp.Spec.GetSources(),
Revisions: []string{"a", "b"},
},
}
testCases := []struct {
name string
multiSource bool
history *struct {
matchesSourceType bool
}
sourceIndex *int32
versionId *int32
expectErrorContains *string
}{
{
name: "single-source app without history, no source index, no version ID",
multiSource: false,
},
{
name: "single-source app without history, no source index, missing version ID",
multiSource: false,
versionId: pointer.Int32(999),
expectErrorContains: pointer.String("the app has no history"),
},
{
name: "single source app without history, present source index, no version ID",
multiSource: false,
sourceIndex: pointer.Int32(0),
},
{
name: "single source app without history, invalid source index, no version ID",
multiSource: false,
sourceIndex: pointer.Int32(999),
expectErrorContains: pointer.String("source index 999 not found"),
},
{
name: "single source app with matching history, no source index, no version ID",
multiSource: false,
history: &struct{ matchesSourceType bool }{true},
},
{
name: "single source app with matching history, no source index, missing version ID",
multiSource: false,
history: &struct{ matchesSourceType bool }{true},
versionId: pointer.Int32(999),
expectErrorContains: pointer.String("history not found for version ID 999"),
},
{
name: "single source app with matching history, no source index, present version ID",
multiSource: false,
history: &struct{ matchesSourceType bool }{true},
versionId: pointer.Int32(1),
},
{
name: "single source app with multi-source history, no source index, no version ID",
multiSource: false,
history: &struct{ matchesSourceType bool }{false},
},
{
name: "single source app with multi-source history, no source index, missing version ID",
multiSource: false,
history: &struct{ matchesSourceType bool }{false},
versionId: pointer.Int32(999),
expectErrorContains: pointer.String("history not found for version ID 999"),
},
{
name: "single source app with multi-source history, no source index, present version ID",
multiSource: false,
history: &struct{ matchesSourceType bool }{false},
versionId: pointer.Int32(1),
},
{
name: "single-source app with multi-source history, source index 1, no version ID",
multiSource: false,
sourceIndex: pointer.Int32(1),
history: &struct{ matchesSourceType bool }{false},
// Since the user requested source index 1, but no version ID, we'll get an error when looking at the live
// source, because the live source is single-source.
expectErrorContains: pointer.String("there is only 1 source"),
},
{
name: "single-source app with multi-source history, invalid source index, no version ID",
multiSource: false,
sourceIndex: pointer.Int32(999),
history: &struct{ matchesSourceType bool }{false},
expectErrorContains: pointer.String("source index 999 not found"),
},
{
name: "single-source app with multi-source history, valid source index, present version ID",
multiSource: false,
sourceIndex: pointer.Int32(1),
history: &struct{ matchesSourceType bool }{false},
versionId: pointer.Int32(1),
},
{
name: "multi-source app without history, no source index, no version ID",
multiSource: true,
},
{
name: "multi-source app without history, no source index, missing version ID",
multiSource: true,
versionId: pointer.Int32(999),
expectErrorContains: pointer.String("the app has no history"),
},
{
name: "multi-source app without history, present source index, no version ID",
multiSource: true,
sourceIndex: pointer.Int32(1),
},
{
name: "multi-source app without history, invalid source index, no version ID",
multiSource: true,
sourceIndex: pointer.Int32(999),
expectErrorContains: pointer.String("source index 999 not found"),
},
{
name: "multi-source app with matching history, no source index, no version ID",
multiSource: true,
history: &struct{ matchesSourceType bool }{true},
},
{
name: "multi-source app with matching history, no source index, missing version ID",
multiSource: true,
history: &struct{ matchesSourceType bool }{true},
versionId: pointer.Int32(999),
expectErrorContains: pointer.String("history not found for version ID 999"),
},
{
name: "multi-source app with matching history, no source index, present version ID",
multiSource: true,
history: &struct{ matchesSourceType bool }{true},
versionId: pointer.Int32(1),
},
{
name: "multi-source app with single-source history, no source index, no version ID",
multiSource: true,
history: &struct{ matchesSourceType bool }{false},
},
{
name: "multi-source app with single-source history, no source index, missing version ID",
multiSource: true,
history: &struct{ matchesSourceType bool }{false},
versionId: pointer.Int32(999),
expectErrorContains: pointer.String("history not found for version ID 999"),
},
{
name: "multi-source app with single-source history, no source index, present version ID",
multiSource: true,
history: &struct{ matchesSourceType bool }{false},
versionId: pointer.Int32(1),
},
{
name: "multi-source app with single-source history, source index 1, no version ID",
multiSource: true,
sourceIndex: pointer.Int32(1),
history: &struct{ matchesSourceType bool }{false},
},
{
name: "multi-source app with single-source history, invalid source index, no version ID",
multiSource: true,
sourceIndex: pointer.Int32(999),
history: &struct{ matchesSourceType bool }{false},
expectErrorContains: pointer.String("source index 999 not found"),
},
{
name: "multi-source app with single-source history, valid source index, present version ID",
multiSource: true,
sourceIndex: pointer.Int32(0),
history: &struct{ matchesSourceType bool }{false},
versionId: pointer.Int32(1),
},
{
name: "multi-source app with single-source history, source index 1, present version ID",
multiSource: true,
sourceIndex: pointer.Int32(1),
history: &struct{ matchesSourceType bool }{false},
versionId: pointer.Int32(1),
expectErrorContains: pointer.String("source index 1 not found"),
},
}
for _, tc := range testCases {
tcc := tc
t.Run(tcc.name, func(t *testing.T) {
app := singleSourceApp
if tcc.multiSource {
app = multiSourceApp
}
if tcc.history != nil {
if tcc.history.matchesSourceType {
if tcc.multiSource {
app.Status.History = multiSourceHistory
} else {
app.Status.History = singleSourceHistory
}
} else {
if tcc.multiSource {
app.Status.History = singleSourceHistory
} else {
app.Status.History = multiSourceHistory
}
}
}
s := newTestAppServer(t, app)
request := &application.RevisionMetadataQuery{
Name: pointer.String(app.Name),
Revision: pointer.String("HEAD"),
SourceIndex: tcc.sourceIndex,
VersionId: tcc.versionId,
}
_, err := s.RevisionMetadata(context.Background(), request)
if tcc.expectErrorContains != nil {
require.ErrorContains(t, err, *tcc.expectErrorContains)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -522,101 +522,6 @@ func TestSimpleListGeneratorGoTemplate(t *testing.T) {
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{*expectedAppNewMetadata}))
}
func TestCreateApplicationDespiteParamsError(t *testing.T) {
expectedErrorMessage := `failed to execute go template {{.cluster}}-guestbook: template: :1:2: executing "" at <.cluster>: map has no entry for key "cluster"`
expectedConditionsParamsError := []v1alpha1.ApplicationSetCondition{
{
Type: v1alpha1.ApplicationSetConditionErrorOccurred,
Status: v1alpha1.ApplicationSetConditionStatusTrue,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonRenderTemplateParamsError,
},
{
Type: v1alpha1.ApplicationSetConditionParametersGenerated,
Status: v1alpha1.ApplicationSetConditionStatusFalse,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
},
{
Type: v1alpha1.ApplicationSetConditionResourcesUpToDate,
Status: v1alpha1.ApplicationSetConditionStatusFalse,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonRenderTemplateParamsError,
},
}
expectedApp := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: application.ApplicationKind,
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-cluster-guestbook",
Namespace: fixture.TestNamespace(),
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: &argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "guestbook",
},
},
}
Given(t).
// Create a ListGenerator-based ApplicationSet
When().Create(v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "simple-list-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
GoTemplate: true,
GoTemplateOptions: []string{"missingkey=error"},
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{.cluster}}-guestbook"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: &argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Server: "{{.url}}",
Namespace: "guestbook",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
List: &v1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{
{
Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
},
{
Raw: []byte(`{"invalidCluster": "invalid-cluster","url": "https://kubernetes.default.svc"}`),
},
},
},
},
},
},
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedApp})).
// verify the ApplicationSet status conditions were set correctly
Expect(ApplicationSetHasConditions("simple-list-generator", expectedConditionsParamsError)).
// Delete the ApplicationSet, and verify it deletes the Applications
When().
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{expectedApp}))
}
func TestRenderHelmValuesObject(t *testing.T) {
expectedApp := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{

View File

@@ -153,7 +153,9 @@ export const ApplicationDeploymentHistory = ({
</React.Fragment>
))
)
) : null}
) : (
<p>Click to see source details.</p>
)}
</div>
</div>
))}

View File

@@ -192,7 +192,14 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
)
);
const getContentForChart = (aRevision: string, aSourceIndex: number, aVersionId: number, indx: number, aSource: models.ApplicationSource, sourceHeader?: JSX.Element) => {
const getContentForChart = (
aRevision: string,
aSourceIndex: number | null,
aVersionId: number | null,
indx: number,
aSource: models.ApplicationSource,
sourceHeader?: JSX.Element
) => {
const showChartNonMetadataInfo = (aRevision: string, aRepoUrl: string) => {
return (
<>
@@ -366,9 +373,9 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
return <>{cont}</>;
} else if (application.spec.source) {
if (source.chart) {
cont.push(getContentForChart(revision, 0, 0, 0, source));
cont.push(getContentForChart(revision, null, null, 0, source));
} else {
cont.push(getContentForNonChart(revision, 0, getAppCurrentVersion(application), 0, source));
cont.push(getContentForNonChart(revision, null, getAppCurrentVersion(application), 0, source));
}
return <>{cont}</>;
} else {

View File

@@ -247,7 +247,7 @@ export const ApplicationParameters = (props: {
</div>
</React.Fragment>
)}
<DataLoader input={app} load={application => getSourceFromSources(application, index)}>
<DataLoader input={app.spec.sources[index]} load={src => getSourceFromAppSources(src, app.metadata.name, app.spec.project, index, 0)}>
{(details: models.RepoAppDetails) => getEditablePanelForOneSource(details, index, source)}
</DataLoader>
</div>
@@ -986,17 +986,12 @@ function gatherDetails(
}
// For Sources field. Get one source with index i from the list
async function getSourceFromSources(app: models.Application, i: number) {
const sources: models.ApplicationSource[] = app.spec.sources;
if (sources && i < sources.length) {
const aSource = sources[i];
const repoDetail = await services.repos.appDetails(aSource, app.metadata.name, app.spec.project, i, 0).catch(() => ({
type: 'Directory' as models.AppSourceType,
path: aSource.path
}));
return repoDetail;
}
return null;
async function getSourceFromAppSources(aSource: models.ApplicationSource, name: string, project: string, index: number, version: number) {
const repoDetail = await services.repos.appDetails(aSource, name, project, index, version).catch(() => ({
type: 'Directory' as models.AppSourceType,
path: aSource.path
}));
return repoDetail;
}
// Delete when source field is removed

View File

@@ -1131,9 +1131,9 @@ export function getAppDefaultOperationSyncRevision(app?: appModels.Application)
// getAppCurrentVersion gets the first app revisions from `status.sync.revisions` or, if that list is missing or empty, the `revision`
// field.
export function getAppCurrentVersion(app?: appModels.Application) {
if (!app || !app.status || !app.status.history) {
return 0;
export function getAppCurrentVersion(app?: appModels.Application): number | null {
if (!app || !app.status || !app.status.history || app.status.history.length === 0) {
return null;
}
return app.status.history[app.status.history.length - 1].id;
}

View File

@@ -857,7 +857,7 @@ export class ReposList extends React.Component<
const confirmed = await this.appContext.apis.popup.confirm('Disconnect repository', `Are you sure you want to disconnect '${repo}'?`);
if (confirmed) {
try {
await services.repos.delete(repo, project);
await services.repos.delete(repo, project || '');
this.repoLoader.reload();
} catch (e) {
this.appContext.apis.notifications.show({

View File

@@ -53,22 +53,26 @@ export class ApplicationsService {
.then(res => res.body as models.ApplicationSyncWindowState);
}
public revisionMetadata(name: string, appNamespace: string, revision: string, sourceIndex: number, versionId: number): Promise<models.RevisionMetadata> {
return requests
.get(`/applications/${name}/revisions/${revision || 'HEAD'}/metadata`)
.query({appNamespace})
.query({sourceIndex})
.query({versionId})
.then(res => res.body as models.RevisionMetadata);
public revisionMetadata(name: string, appNamespace: string, revision: string, sourceIndex: number | null, versionId: number | null): Promise<models.RevisionMetadata> {
let r = requests.get(`/applications/${name}/revisions/${revision || 'HEAD'}/metadata`).query({appNamespace});
if (sourceIndex !== null) {
r = r.query({sourceIndex});
}
if (versionId !== null) {
r = r.query({versionId});
}
return r.then(res => res.body as models.RevisionMetadata);
}
public revisionChartDetails(name: string, appNamespace: string, revision: string, sourceIndex: number, versionId: number): Promise<models.ChartDetails> {
return requests
.get(`/applications/${name}/revisions/${revision || 'HEAD'}/chartdetails`)
.query({appNamespace})
.query({sourceIndex})
.query({versionId})
.then(res => res.body as models.ChartDetails);
let r = requests.get(`/applications/${name}/revisions/${revision || 'HEAD'}/chartdetails`).query({appNamespace});
if (sourceIndex !== null) {
r = r.query({sourceIndex});
}
if (versionId !== null) {
r = r.query({versionId});
}
return r.then(res => res.body as models.ChartDetails);
}
public resourceTree(name: string, appNamespace: string): Promise<models.ApplicationTree> {

View File

@@ -112,12 +112,12 @@ func GetAppRefreshPaths(app *v1alpha1.Application) []string {
}
// AppFilesHaveChanged returns true if any of the changed files are under the given refresh paths
// If refreshPaths is empty, it will always return true
// If refreshPaths or changedFiles are empty, it will always return true
func AppFilesHaveChanged(refreshPaths []string, changedFiles []string) bool {
// empty slice means there was no changes to any files
// so we should not refresh
// an empty slice of changed files means that the payload didn't include a list
// of changed files and we have to assume that a refresh is required
if len(changedFiles) == 0 {
return false
return true
}
if len(refreshPaths) == 0 {

View File

@@ -134,7 +134,7 @@ func Test_AppFilesHaveChanged(t *testing.T) {
changeExpected bool
}{
{"default no path", &v1alpha1.Application{}, []string{"README.md"}, true},
{"no files changed", getApp(".", "source/path"), []string{}, false},
{"no files changed", getApp(".", "source/path"), []string{}, true},
{"relative path - matching", getApp(".", "source/path"), []string{"source/path/my-deployment.yaml"}, true},
{"relative path, multi source - matching #1", getMultiSourceApp(".", "source/path", "other/path"), []string{"source/path/my-deployment.yaml"}, true},
{"relative path, multi source - matching #2", getMultiSourceApp(".", "other/path", "source/path"), []string{"source/path/my-deployment.yaml"}, true},