mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 17:07:16 +00:00
2219 lines
76 KiB
Go
2219 lines
76 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"dario.cat/mergo"
|
|
cachemocks "github.com/argoproj/argo-cd/gitops-engine/pkg/cache/mocks"
|
|
"github.com/argoproj/argo-cd/gitops-engine/pkg/health"
|
|
synccommon "github.com/argoproj/argo-cd/gitops-engine/pkg/sync/common"
|
|
"github.com/argoproj/argo-cd/gitops-engine/pkg/utils/kube"
|
|
. "github.com/argoproj/argo-cd/gitops-engine/pkg/utils/testing"
|
|
"github.com/sirupsen/logrus"
|
|
logrustest "github.com/sirupsen/logrus/hooks/test"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
networkingv1 "k8s.io/api/networking/v1"
|
|
rbacv1 "k8s.io/api/rbac/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
|
|
"github.com/argoproj/argo-cd/v3/common"
|
|
"github.com/argoproj/argo-cd/v3/controller/testdata"
|
|
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
|
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
|
|
"github.com/argoproj/argo-cd/v3/reposerver/apiclient/mocks"
|
|
"github.com/argoproj/argo-cd/v3/test"
|
|
)
|
|
|
|
// TestCompareAppStateEmpty tests comparison when both git and live have no objects
|
|
func TestCompareAppStateEmpty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := newFakeApp()
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
|
assert.Empty(t, compRes.resources)
|
|
assert.Empty(t, compRes.managedResources)
|
|
assert.Empty(t, app.Status.Conditions)
|
|
}
|
|
|
|
// TestCompareAppStateRepoError tests the case when CompareAppState notices a repo error
|
|
func TestCompareAppStateRepoError(t *testing.T) {
|
|
app := newFakeApp()
|
|
ctrl := newFakeController(t.Context(), &fakeData{manifestResponses: make([]*apiclient.ManifestResponse, 3)}, errors.New("test repo error"))
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
assert.Nil(t, compRes)
|
|
require.EqualError(t, err, ErrCompareStateRepo.Error())
|
|
|
|
// expect to still get compare state error to as inside grace period
|
|
compRes, err = ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
assert.Nil(t, compRes)
|
|
require.EqualError(t, err, ErrCompareStateRepo.Error())
|
|
|
|
time.Sleep(10 * time.Second)
|
|
// expect to not get error as outside of grace period, but status should be unknown
|
|
compRes, err = ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
assert.NotNil(t, compRes)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
|
|
}
|
|
|
|
// TestCompareAppStateNamespaceMetadataDiffers tests comparison when managed namespace metadata differs
|
|
func TestCompareAppStateNamespaceMetadataDiffers(t *testing.T) {
|
|
app := newFakeApp()
|
|
app.Spec.SyncPolicy.ManagedNamespaceMetadata = &v1alpha1.ManagedNamespaceMetadata{
|
|
Labels: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
}
|
|
app.Status.OperationState = &v1alpha1.OperationState{
|
|
SyncResult: &v1alpha1.SyncOperationResult{},
|
|
}
|
|
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
|
|
assert.Empty(t, compRes.resources)
|
|
assert.Empty(t, compRes.managedResources)
|
|
assert.Empty(t, app.Status.Conditions)
|
|
}
|
|
|
|
// 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 = &v1alpha1.ManagedNamespaceMetadata{
|
|
Labels: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
}
|
|
app.Status.OperationState = &v1alpha1.OperationState{
|
|
SyncResult: &v1alpha1.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(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.Equal(t, v1alpha1.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()
|
|
require.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.Empty(t, app.Status.Conditions)
|
|
}
|
|
|
|
// 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 = &v1alpha1.ManagedNamespaceMetadata{
|
|
Labels: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
}
|
|
app.Status.OperationState = &v1alpha1.OperationState{
|
|
SyncResult: &v1alpha1.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(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.Equal(t, v1alpha1.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()
|
|
require.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.Empty(t, app.Status.Conditions)
|
|
}
|
|
|
|
// TestCompareAppStateNamespaceMetadataIsTheSame tests comparison when managed namespace metadata is the same
|
|
func TestCompareAppStateNamespaceMetadataIsTheSame(t *testing.T) {
|
|
app := newFakeApp()
|
|
app.Spec.SyncPolicy.ManagedNamespaceMetadata = &v1alpha1.ManagedNamespaceMetadata{
|
|
Labels: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
}
|
|
app.Status.OperationState = &v1alpha1.OperationState{
|
|
SyncResult: &v1alpha1.SyncOperationResult{
|
|
ManagedNamespaceMetadata: &v1alpha1.ManagedNamespaceMetadata{
|
|
Labels: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
|
assert.Empty(t, compRes.resources)
|
|
assert.Empty(t, compRes.managedResources)
|
|
assert.Empty(t, app.Status.Conditions)
|
|
}
|
|
|
|
// TestCompareAppStateMissing tests when there is a manifest defined in the repo which doesn't exist in live
|
|
func TestCompareAppStateMissing(t *testing.T) {
|
|
app := newFakeApp()
|
|
data := fakeData{
|
|
apps: []runtime.Object{app},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{PodManifest},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
|
|
assert.Len(t, compRes.resources, 1)
|
|
assert.Len(t, compRes.managedResources, 1)
|
|
assert.Empty(t, app.Status.Conditions)
|
|
}
|
|
|
|
// TestCompareAppStateExtra tests when there is an extra object in live but not defined in git
|
|
func TestCompareAppStateExtra(t *testing.T) {
|
|
pod := NewPod()
|
|
pod.SetNamespace(test.FakeDestNamespace)
|
|
app := newFakeApp()
|
|
key := kube.ResourceKey{Group: "", Kind: "Pod", Namespace: test.FakeDestNamespace, Name: app.Name}
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
|
key: pod,
|
|
},
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
|
|
assert.Len(t, compRes.resources, 1)
|
|
assert.Len(t, compRes.managedResources, 1)
|
|
assert.Empty(t, app.Status.Conditions)
|
|
}
|
|
|
|
// TestCompareAppStateHook checks that hooks are detected during manifest generation, and not
|
|
// considered as part of resources when assessing Synced status
|
|
func TestCompareAppStateHook(t *testing.T) {
|
|
pod := NewPod()
|
|
pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "PreSync"})
|
|
podBytes, _ := json.Marshal(pod)
|
|
app := newFakeApp()
|
|
data := fakeData{
|
|
apps: []runtime.Object{app},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{string(podBytes)},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
|
assert.Empty(t, compRes.resources)
|
|
assert.Empty(t, compRes.managedResources)
|
|
assert.Len(t, compRes.reconciliationResult.Hooks, 1)
|
|
assert.Empty(t, app.Status.Conditions)
|
|
}
|
|
|
|
// TestCompareAppStateSkipHook checks that skipped resources are detected during manifest generation, and not
|
|
// considered as part of resources when assessing Synced status
|
|
func TestCompareAppStateSkipHook(t *testing.T) {
|
|
pod := NewPod()
|
|
pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "Skip"})
|
|
podBytes, _ := json.Marshal(pod)
|
|
app := newFakeApp()
|
|
data := fakeData{
|
|
apps: []runtime.Object{app},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{string(podBytes)},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
|
assert.Len(t, compRes.resources, 1)
|
|
assert.Len(t, compRes.managedResources, 1)
|
|
assert.Empty(t, compRes.reconciliationResult.Hooks)
|
|
assert.Empty(t, app.Status.Conditions)
|
|
}
|
|
|
|
// TestCompareAppStateSyncHookSyncWave tests that Sync hooks display correct SyncWave
|
|
// This is the specific case from issue #26208
|
|
func TestCompareAppStateSyncHookSyncWave(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hookType string
|
|
syncWave string
|
|
expectedSyncWave int64
|
|
}{
|
|
{
|
|
name: "Sync hook with wave 2",
|
|
hookType: "Sync",
|
|
syncWave: "2",
|
|
expectedSyncWave: 2,
|
|
},
|
|
{
|
|
name: "PreSync hook with wave 1",
|
|
hookType: "PreSync",
|
|
syncWave: "1",
|
|
expectedSyncWave: 1,
|
|
},
|
|
{
|
|
name: "PostSync hook with negative wave",
|
|
hookType: "PostSync",
|
|
syncWave: "-1",
|
|
expectedSyncWave: -1,
|
|
},
|
|
{
|
|
name: "Sync hook without explicit wave",
|
|
hookType: "Sync",
|
|
syncWave: "",
|
|
expectedSyncWave: 0, // default
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
app := newFakeApp()
|
|
|
|
// Create hook pod with annotations
|
|
hookPod := NewPod()
|
|
hookPod.SetNamespace(test.FakeDestNamespace)
|
|
annot := map[string]string{
|
|
synccommon.AnnotationKeyHook: tt.hookType,
|
|
}
|
|
if tt.syncWave != "" {
|
|
annot[synccommon.AnnotationSyncWave] = tt.syncWave
|
|
}
|
|
hookPod.SetAnnotations(annot)
|
|
|
|
// The hook exists in live state (already created by previous sync)
|
|
livePod := hookPod.DeepCopy()
|
|
|
|
data := fakeData{
|
|
apps: []runtime.Object{app},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{toJSON(t, hookPod)},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
|
kube.GetResourceKey(livePod): livePod,
|
|
},
|
|
}
|
|
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := []v1alpha1.ApplicationSource{app.Spec.GetSource()}
|
|
revisions := []string{""}
|
|
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, compRes)
|
|
|
|
// For hooks, they go into reconciliationResult.Hooks, not resources
|
|
// But we should also check resources if the hook appears there
|
|
for _, res := range compRes.resources {
|
|
if res.Hook {
|
|
assert.Equal(t, tt.expectedSyncWave, res.SyncWave,
|
|
"Hook SyncWave should be %d but got %d", tt.expectedSyncWave, res.SyncWave)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompareAppStateRequireDeletion(t *testing.T) {
|
|
obj1 := NewPod()
|
|
obj1.SetName("my-pod-1")
|
|
obj1.SetAnnotations(map[string]string{"argocd.argoproj.io/sync-options": "Delete=confirm"})
|
|
obj2 := NewPod()
|
|
obj2.SetName("my-pod-2")
|
|
obj2.SetAnnotations(map[string]string{"argocd.argoproj.io/sync-options": "Prune=confirm"})
|
|
obj3 := NewPod()
|
|
obj3.SetName("my-pod-3")
|
|
|
|
app := newFakeApp()
|
|
data := fakeData{
|
|
apps: []runtime.Object{app},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{toJSON(t, obj1), toJSON(t, obj2), toJSON(t, obj3)},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
|
kube.GetResourceKey(obj1): obj1,
|
|
kube.GetResourceKey(obj2): obj2,
|
|
kube.GetResourceKey(obj3): obj3,
|
|
},
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
|
|
assert.Len(t, compRes.resources, 3)
|
|
assert.Len(t, compRes.managedResources, 3)
|
|
assert.Empty(t, app.Status.Conditions)
|
|
|
|
countRequireDeletion := 0
|
|
for _, res := range compRes.resources {
|
|
if res.RequiresDeletionConfirmation {
|
|
countRequireDeletion++
|
|
}
|
|
}
|
|
assert.Equal(t, 2, countRequireDeletion)
|
|
}
|
|
|
|
// checks that ignore resources are detected, but excluded from status
|
|
func TestCompareAppStateCompareOptionIgnoreExtraneous(t *testing.T) {
|
|
pod := NewPod()
|
|
pod.SetAnnotations(map[string]string{common.AnnotationCompareOptions: "IgnoreExtraneous"})
|
|
app := newFakeApp()
|
|
data := fakeData{
|
|
apps: []runtime.Object{app},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
assert.NotNil(t, compRes)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
|
assert.Empty(t, compRes.resources)
|
|
assert.Empty(t, compRes.managedResources)
|
|
assert.Empty(t, app.Status.Conditions)
|
|
}
|
|
|
|
// TestCompareAppStateExtraHook tests when there is an extra _hook_ object in live but not defined in git
|
|
func TestCompareAppStateExtraHook(t *testing.T) {
|
|
pod := NewPod()
|
|
pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "PreSync"})
|
|
pod.SetNamespace(test.FakeDestNamespace)
|
|
app := newFakeApp()
|
|
key := kube.ResourceKey{Group: "", Kind: "Pod", Namespace: test.FakeDestNamespace, Name: app.Name}
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
|
key: pod,
|
|
},
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
assert.NotNil(t, compRes)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
|
assert.Len(t, compRes.resources, 1)
|
|
assert.Len(t, compRes.managedResources, 1)
|
|
assert.Empty(t, compRes.reconciliationResult.Hooks)
|
|
assert.Empty(t, app.Status.Conditions)
|
|
}
|
|
|
|
// TestAppRevisions tests that revisions are properly propagated for a single source app
|
|
func TestAppRevisionsSingleSource(t *testing.T) {
|
|
obj1 := NewPod()
|
|
obj1.SetNamespace(test.FakeDestNamespace)
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{toJSON(t, obj1)},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
|
|
app := newFakeApp()
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, app.Spec.GetSources(), false, false, nil, app.Spec.HasMultipleSources())
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.NotEmpty(t, compRes.syncStatus.Revision)
|
|
assert.Empty(t, compRes.syncStatus.Revisions)
|
|
}
|
|
|
|
// TestAppRevisions tests that revisions are properly propagated for a multi source app
|
|
func TestAppRevisionsMultiSource(t *testing.T) {
|
|
obj1 := NewPod()
|
|
obj1.SetNamespace(test.FakeDestNamespace)
|
|
data := fakeData{
|
|
manifestResponses: []*apiclient.ManifestResponse{
|
|
{
|
|
Manifests: []string{toJSON(t, obj1)},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
{
|
|
Manifests: []string{toJSON(t, obj1)},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "def456",
|
|
},
|
|
{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "ghi789",
|
|
},
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
|
|
app := newFakeMultiSourceApp()
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, app.Spec.GetSources(), false, false, nil, app.Spec.HasMultipleSources())
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.Empty(t, compRes.syncStatus.Revision)
|
|
assert.Len(t, compRes.syncStatus.Revisions, 3)
|
|
assert.Equal(t, "abc123", compRes.syncStatus.Revisions[0])
|
|
assert.Equal(t, "def456", compRes.syncStatus.Revisions[1])
|
|
assert.Equal(t, "ghi789", compRes.syncStatus.Revisions[2])
|
|
}
|
|
|
|
func toJSON(t *testing.T, obj *unstructured.Unstructured) string {
|
|
t.Helper()
|
|
data, err := json.Marshal(obj)
|
|
require.NoError(t, err)
|
|
return string(data)
|
|
}
|
|
|
|
func TestCompareAppStateDuplicatedNamespacedResources(t *testing.T) {
|
|
obj1 := NewPod()
|
|
obj1.SetNamespace(test.FakeDestNamespace)
|
|
obj2 := NewPod()
|
|
obj3 := NewPod()
|
|
obj3.SetNamespace("kube-system")
|
|
obj4 := NewPod()
|
|
obj4.SetGenerateName("my-pod")
|
|
obj4.SetName("")
|
|
obj5 := NewPod()
|
|
obj5.SetName("")
|
|
obj5.SetGenerateName("my-pod")
|
|
|
|
app := newFakeApp()
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{toJSON(t, obj1), toJSON(t, obj2), toJSON(t, obj3), toJSON(t, obj4), toJSON(t, obj5)},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
|
kube.GetResourceKey(obj1): obj1,
|
|
kube.GetResourceKey(obj3): obj3,
|
|
},
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
assert.NotNil(t, compRes)
|
|
assert.Len(t, app.Status.Conditions, 1)
|
|
assert.NotNil(t, app.Status.Conditions[0].LastTransitionTime)
|
|
assert.Equal(t, v1alpha1.ApplicationConditionRepeatedResourceWarning, app.Status.Conditions[0].Type)
|
|
assert.Equal(t, "Resource /Pod/fake-dest-ns/my-pod appeared 2 times among application resources.", app.Status.Conditions[0].Message)
|
|
assert.Len(t, compRes.resources, 4)
|
|
}
|
|
|
|
func TestCompareAppStateManagedNamespaceMetadataWithLiveNsDoesNotGetPruned(t *testing.T) {
|
|
app := newFakeApp()
|
|
app.Spec.SyncPolicy = &v1alpha1.SyncPolicy{
|
|
ManagedNamespaceMetadata: &v1alpha1.ManagedNamespaceMetadata{
|
|
Labels: nil,
|
|
Annotations: nil,
|
|
},
|
|
}
|
|
|
|
ns := NewNamespace()
|
|
ns.SetName(test.FakeDestNamespace)
|
|
ns.SetNamespace(test.FakeDestNamespace)
|
|
ns.SetAnnotations(map[string]string{"argocd.argoproj.io/sync-options": "ServerSideApply=true"})
|
|
|
|
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(t.Context(), &data, nil)
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, []string{}, app.Spec.Sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
assert.NotNil(t, compRes)
|
|
assert.Empty(t, app.Status.Conditions)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
// Ensure that ns does not get pruned
|
|
assert.NotNil(t, compRes.reconciliationResult.Target[0])
|
|
assert.Equal(t, compRes.reconciliationResult.Target[0].GetName(), ns.GetName())
|
|
assert.Equal(t, compRes.reconciliationResult.Target[0].GetAnnotations(), ns.GetAnnotations())
|
|
assert.Equal(t, compRes.reconciliationResult.Target[0].GetLabels(), ns.GetLabels())
|
|
assert.Len(t, compRes.resources, 1)
|
|
assert.Len(t, compRes.managedResources, 1)
|
|
}
|
|
|
|
var defaultProj = v1alpha1.AppProject{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "default",
|
|
Namespace: test.FakeArgoCDNamespace,
|
|
},
|
|
Spec: v1alpha1.AppProjectSpec{
|
|
SourceRepos: []string{"*"},
|
|
Destinations: []v1alpha1.ApplicationDestination{
|
|
{
|
|
Server: "*",
|
|
Namespace: "*",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// TestCompareAppStateWithManifestGeneratePath tests that it compares revisions when the manifest-generate-path annotation is set.
|
|
func TestCompareAppStateWithManifestGeneratePath(t *testing.T) {
|
|
app := newFakeApp()
|
|
app.SetAnnotations(map[string]string{v1alpha1.AnnotationKeyManifestGeneratePaths: "."})
|
|
app.Status.Sync = v1alpha1.SyncStatus{
|
|
Revision: "abc123",
|
|
Status: v1alpha1.SyncStatusCodeSynced,
|
|
}
|
|
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
updateRevisionForPathsResponse: &apiclient.UpdateRevisionForPathsResponse{},
|
|
}
|
|
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "abc123")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, app.Spec.GetSources(), false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
|
assert.Equal(t, "abc123", compRes.syncStatus.Revision)
|
|
}
|
|
|
|
func TestSetHealth(t *testing.T) {
|
|
app := newFakeApp()
|
|
deployment := kube.MustToUnstructured(&appsv1.Deployment{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "demo",
|
|
Namespace: "default",
|
|
},
|
|
})
|
|
ctrl := newFakeController(t.Context(), &fakeData{
|
|
apps: []runtime.Object{app, &defaultProj},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
|
kube.GetResourceKey(deployment): deployment,
|
|
},
|
|
}, nil)
|
|
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, health.HealthStatusHealthy, compRes.healthStatus)
|
|
}
|
|
|
|
func TestPreserveStatusTimestamp(t *testing.T) {
|
|
timestamp := metav1.Now()
|
|
app := newFakeAppWithHealthAndTime(health.HealthStatusHealthy, timestamp)
|
|
deployment := kube.MustToUnstructured(&appsv1.Deployment{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "demo",
|
|
Namespace: "default",
|
|
},
|
|
})
|
|
ctrl := newFakeController(t.Context(), &fakeData{
|
|
apps: []runtime.Object{app, &defaultProj},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
|
kube.GetResourceKey(deployment): deployment,
|
|
},
|
|
}, nil)
|
|
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, health.HealthStatusHealthy, compRes.healthStatus)
|
|
}
|
|
|
|
func TestSetHealthSelfReferencedApp(t *testing.T) {
|
|
app := newFakeApp()
|
|
unstructuredApp := kube.MustToUnstructured(app)
|
|
deployment := kube.MustToUnstructured(&appsv1.Deployment{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "demo",
|
|
Namespace: "default",
|
|
},
|
|
})
|
|
ctrl := newFakeController(t.Context(), &fakeData{
|
|
apps: []runtime.Object{app, &defaultProj},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
|
kube.GetResourceKey(deployment): deployment,
|
|
kube.GetResourceKey(unstructuredApp): unstructuredApp,
|
|
},
|
|
}, nil)
|
|
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, health.HealthStatusHealthy, compRes.healthStatus)
|
|
}
|
|
|
|
func TestSetManagedResourcesWithOrphanedResources(t *testing.T) {
|
|
proj := defaultProj.DeepCopy()
|
|
proj.Spec.OrphanedResources = &v1alpha1.OrphanedResourcesMonitorSettings{}
|
|
|
|
app := newFakeApp()
|
|
ctrl := newFakeController(t.Context(), &fakeData{
|
|
apps: []runtime.Object{app, proj},
|
|
namespacedResources: map[kube.ResourceKey]namespacedResource{
|
|
kube.NewResourceKey("apps", kube.DeploymentKind, app.Namespace, "guestbook"): {
|
|
ResourceNode: v1alpha1.ResourceNode{
|
|
ResourceRef: v1alpha1.ResourceRef{Kind: kube.DeploymentKind, Name: "guestbook", Namespace: app.Namespace},
|
|
},
|
|
AppName: "",
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
tree, err := ctrl.setAppManagedResources(&v1alpha1.Cluster{Server: "test", Name: "test"}, app, &comparisonResult{managedResources: make([]managedResource, 0)})
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, tree.OrphanedNodes, 1)
|
|
assert.Equal(t, "guestbook", tree.OrphanedNodes[0].Name)
|
|
assert.Equal(t, app.Namespace, tree.OrphanedNodes[0].Namespace)
|
|
}
|
|
|
|
func TestSetManagedResourcesWithResourcesOfAnotherApp(t *testing.T) {
|
|
proj := defaultProj.DeepCopy()
|
|
proj.Spec.OrphanedResources = &v1alpha1.OrphanedResourcesMonitorSettings{}
|
|
|
|
app1 := newFakeApp()
|
|
app1.Name = "app1"
|
|
app2 := newFakeApp()
|
|
app2.Name = "app2"
|
|
|
|
ctrl := newFakeController(t.Context(), &fakeData{
|
|
apps: []runtime.Object{app1, app2, proj},
|
|
namespacedResources: map[kube.ResourceKey]namespacedResource{
|
|
kube.NewResourceKey("apps", kube.DeploymentKind, app2.Namespace, "guestbook"): {
|
|
ResourceNode: v1alpha1.ResourceNode{
|
|
ResourceRef: v1alpha1.ResourceRef{Kind: kube.DeploymentKind, Name: "guestbook", Namespace: app2.Namespace},
|
|
},
|
|
AppName: "app2",
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
tree, err := ctrl.setAppManagedResources(&v1alpha1.Cluster{Server: "test", Name: "test"}, app1, &comparisonResult{managedResources: make([]managedResource, 0)})
|
|
|
|
require.NoError(t, err)
|
|
assert.Empty(t, tree.OrphanedNodes)
|
|
}
|
|
|
|
func TestReturnUnknownComparisonStateOnSettingLoadError(t *testing.T) {
|
|
proj := defaultProj.DeepCopy()
|
|
proj.Spec.OrphanedResources = &v1alpha1.OrphanedResourcesMonitorSettings{}
|
|
|
|
app := newFakeApp()
|
|
|
|
ctrl := newFakeController(t.Context(), &fakeData{
|
|
apps: []runtime.Object{app, proj},
|
|
configMapData: map[string]string{
|
|
"resource.customizations": "invalid setting",
|
|
},
|
|
}, nil)
|
|
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, health.HealthStatusUnknown, compRes.healthStatus)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
|
|
}
|
|
|
|
func TestSetManagedResourcesKnownOrphanedResourceExceptions(t *testing.T) {
|
|
proj := defaultProj.DeepCopy()
|
|
proj.Spec.OrphanedResources = &v1alpha1.OrphanedResourcesMonitorSettings{}
|
|
proj.Spec.SourceNamespaces = []string{"default"}
|
|
|
|
app := newFakeApp()
|
|
app.Namespace = "default"
|
|
|
|
ctrl := newFakeController(t.Context(), &fakeData{
|
|
apps: []runtime.Object{app, proj},
|
|
namespacedResources: map[kube.ResourceKey]namespacedResource{
|
|
kube.NewResourceKey("apps", kube.DeploymentKind, app.Namespace, "guestbook"): {
|
|
ResourceNode: v1alpha1.ResourceNode{ResourceRef: v1alpha1.ResourceRef{Group: "apps", Kind: kube.DeploymentKind, Name: "guestbook", Namespace: app.Namespace}},
|
|
},
|
|
kube.NewResourceKey("", kube.ServiceAccountKind, app.Namespace, "default"): {
|
|
ResourceNode: v1alpha1.ResourceNode{ResourceRef: v1alpha1.ResourceRef{Kind: kube.ServiceAccountKind, Name: "default", Namespace: app.Namespace}},
|
|
},
|
|
kube.NewResourceKey("", kube.ServiceKind, app.Namespace, "kubernetes"): {
|
|
ResourceNode: v1alpha1.ResourceNode{ResourceRef: v1alpha1.ResourceRef{Kind: kube.ServiceAccountKind, Name: "kubernetes", Namespace: app.Namespace}},
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
tree, err := ctrl.setAppManagedResources(&v1alpha1.Cluster{Server: "test", Name: "test"}, app, &comparisonResult{managedResources: make([]managedResource, 0)})
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, tree.OrphanedNodes, 1)
|
|
assert.Equal(t, "guestbook", tree.OrphanedNodes[0].Name)
|
|
}
|
|
|
|
func Test_appStateManager_persistRevisionHistory(t *testing.T) {
|
|
app := newFakeApp()
|
|
ctrl := newFakeController(t.Context(), &fakeData{
|
|
apps: []runtime.Object{app},
|
|
}, nil)
|
|
manager := ctrl.appStateManager.(*appStateManager)
|
|
setRevisionHistoryLimit := func(value int) {
|
|
if value < 0 {
|
|
value = 0
|
|
}
|
|
i := int64(value)
|
|
app.Spec.RevisionHistoryLimit = &i
|
|
}
|
|
addHistory := func() {
|
|
err := manager.persistRevisionHistory(app, "my-revision", v1alpha1.ApplicationSource{}, []string{}, []v1alpha1.ApplicationSource{}, false, metav1.Time{}, v1alpha1.OperationInitiator{})
|
|
require.NoError(t, err)
|
|
}
|
|
addHistory()
|
|
assert.Len(t, app.Status.History, 1)
|
|
addHistory()
|
|
assert.Len(t, app.Status.History, 2)
|
|
addHistory()
|
|
assert.Len(t, app.Status.History, 3)
|
|
addHistory()
|
|
assert.Len(t, app.Status.History, 4)
|
|
addHistory()
|
|
assert.Len(t, app.Status.History, 5)
|
|
addHistory()
|
|
assert.Len(t, app.Status.History, 6)
|
|
addHistory()
|
|
assert.Len(t, app.Status.History, 7)
|
|
addHistory()
|
|
assert.Len(t, app.Status.History, 8)
|
|
addHistory()
|
|
assert.Len(t, app.Status.History, 9)
|
|
addHistory()
|
|
assert.Len(t, app.Status.History, 10)
|
|
// default limit is 10
|
|
addHistory()
|
|
assert.Len(t, app.Status.History, 10)
|
|
// increase limit
|
|
setRevisionHistoryLimit(11)
|
|
addHistory()
|
|
assert.Len(t, app.Status.History, 11)
|
|
// decrease limit
|
|
setRevisionHistoryLimit(9)
|
|
addHistory()
|
|
assert.Len(t, app.Status.History, 9)
|
|
|
|
metav1NowTime := metav1.NewTime(time.Now())
|
|
err := manager.persistRevisionHistory(app, "my-revision", v1alpha1.ApplicationSource{}, []string{}, []v1alpha1.ApplicationSource{}, false, metav1NowTime, v1alpha1.OperationInitiator{})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, app.Status.History.LastRevisionHistory().DeployStartedAt, &metav1NowTime)
|
|
|
|
// negative limit to 0
|
|
setRevisionHistoryLimit(-1)
|
|
addHistory()
|
|
assert.Empty(t, app.Status.History)
|
|
}
|
|
|
|
var projWithSourceIntegrity = v1alpha1.AppProject{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "default",
|
|
Namespace: test.FakeArgoCDNamespace,
|
|
},
|
|
Spec: v1alpha1.AppProjectSpec{
|
|
SourceRepos: []string{"*"},
|
|
Destinations: []v1alpha1.ApplicationDestination{
|
|
{
|
|
Server: "*",
|
|
Namespace: "*",
|
|
},
|
|
},
|
|
SourceIntegrity: &v1alpha1.SourceIntegrity{
|
|
Git: &v1alpha1.SourceIntegrityGit{
|
|
Policies: []*v1alpha1.SourceIntegrityGitPolicy{{
|
|
GPG: &v1alpha1.SourceIntegrityGitPolicyGPG{
|
|
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeStrict,
|
|
Keys: []string{"4AEE18F83AFDEB23"},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
func TestNoSourceIntegrity(t *testing.T) {
|
|
t.Setenv("ARGOCD_GPG_ENABLED", "true")
|
|
|
|
{
|
|
app := newFakeApp()
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
SourceIntegrityResult: nil, // No verification requested
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
|
assert.Empty(t, compRes.resources)
|
|
assert.Empty(t, compRes.managedResources)
|
|
assert.Empty(t, app.Status.Conditions)
|
|
}
|
|
}
|
|
|
|
func TestValidSourceIntegrity(t *testing.T) {
|
|
t.Setenv("ARGOCD_GPG_ENABLED", "true")
|
|
|
|
// Source integrity required, and it is valid - sync!
|
|
{
|
|
app := newFakeApp()
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
SourceIntegrityResult: &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
|
|
Name: "Some/check",
|
|
Problems: []string{}, // Valid
|
|
}}},
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &projWithSourceIntegrity, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
|
assert.Empty(t, compRes.resources)
|
|
assert.Empty(t, compRes.managedResources)
|
|
assert.Empty(t, app.Status.Conditions)
|
|
}
|
|
// Source integrity required, not valid - do not sync
|
|
{
|
|
app := newFakeApp()
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
SourceIntegrityResult: &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
|
|
Name: "Some/check",
|
|
Problems: []string{"The thing have failed to validate!"},
|
|
}}},
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "abc123")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &projWithSourceIntegrity, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
|
assert.Empty(t, compRes.resources)
|
|
assert.Empty(t, compRes.managedResources)
|
|
require.Len(t, app.Status.Conditions, 1)
|
|
assert.Contains(t, app.Status.Conditions[0].Message, "Some/check: The thing have failed to validate!")
|
|
}
|
|
|
|
// Source integrity required, unknown key - do not sync
|
|
{
|
|
app := newFakeApp()
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
SourceIntegrityResult: &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
|
|
Name: "Some/check",
|
|
Problems: []string{"The thing have failed to validate!"},
|
|
}}},
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "abc123")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &projWithSourceIntegrity, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
|
assert.Empty(t, compRes.resources)
|
|
assert.Empty(t, compRes.managedResources)
|
|
require.Len(t, app.Status.Conditions, 1)
|
|
assert.Contains(t, app.Status.Conditions[0].Message, "The thing have failed to validate!")
|
|
}
|
|
|
|
t.Setenv("ARGOCD_GPG_ENABLED", "false")
|
|
|
|
// Signature required and local manifests supplied and GPG subsystem is disabled - sync
|
|
{
|
|
app := newFakeApp()
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
SourceIntegrityResult: &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
|
|
Name: "Some/check",
|
|
Problems: []string{"The thing have failed to validate!"},
|
|
}}},
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
// it doesn't matter for our test whether local manifests are valid
|
|
localManifests := []string{""}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "abc123")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &projWithSourceIntegrity, revisions, sources, false, false, localManifests, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
|
assert.Empty(t, compRes.resources)
|
|
assert.Empty(t, compRes.managedResources)
|
|
assert.Empty(t, app.Status.Conditions)
|
|
}
|
|
}
|
|
|
|
func TestComparisonResult_GetHealthStatus(t *testing.T) {
|
|
status := health.HealthStatusMissing
|
|
res := comparisonResult{
|
|
healthStatus: status,
|
|
}
|
|
|
|
assert.Equal(t, status, res.GetHealthStatus())
|
|
}
|
|
|
|
func TestComparisonResult_GetSyncStatus(t *testing.T) {
|
|
status := &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync}
|
|
res := comparisonResult{
|
|
syncStatus: status,
|
|
}
|
|
|
|
assert.Equal(t, status, res.GetSyncStatus())
|
|
}
|
|
|
|
func TestIsLiveResourceManaged(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
managedObj := kube.MustToUnstructured(&corev1.ConfigMap{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "configmap1",
|
|
Namespace: "default",
|
|
Annotations: map[string]string{
|
|
common.AnnotationKeyAppInstance: "guestbook:/ConfigMap:default/configmap1",
|
|
},
|
|
},
|
|
})
|
|
managedObjWithLabel := kube.MustToUnstructured(&corev1.ConfigMap{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "configmap1",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
common.LabelKeyAppInstance: "guestbook",
|
|
},
|
|
},
|
|
})
|
|
unmanagedObjWrongName := kube.MustToUnstructured(&corev1.ConfigMap{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "configmap2",
|
|
Namespace: "default",
|
|
Annotations: map[string]string{
|
|
common.AnnotationKeyAppInstance: "guestbook:/ConfigMap:default/configmap1",
|
|
},
|
|
},
|
|
})
|
|
unmanagedObjWrongKind := kube.MustToUnstructured(&corev1.ConfigMap{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "configmap2",
|
|
Namespace: "default",
|
|
Annotations: map[string]string{
|
|
common.AnnotationKeyAppInstance: "guestbook:/Service:default/configmap2",
|
|
},
|
|
},
|
|
})
|
|
unmanagedObjWrongGroup := kube.MustToUnstructured(&corev1.ConfigMap{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "configmap2",
|
|
Namespace: "default",
|
|
Annotations: map[string]string{
|
|
common.AnnotationKeyAppInstance: "guestbook:apps/ConfigMap:default/configmap2",
|
|
},
|
|
},
|
|
})
|
|
unmanagedObjWrongNamespace := kube.MustToUnstructured(&corev1.ConfigMap{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "configmap2",
|
|
Namespace: "default",
|
|
Annotations: map[string]string{
|
|
common.AnnotationKeyAppInstance: "guestbook:/ConfigMap:fakens/configmap2",
|
|
},
|
|
},
|
|
})
|
|
managedWrongAPIGroup := kube.MustToUnstructured(&networkingv1.Ingress{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: "networking.k8s.io/v1",
|
|
Kind: "Ingress",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "some-ingress",
|
|
Namespace: "default",
|
|
Annotations: map[string]string{
|
|
common.AnnotationKeyAppInstance: "guestbook:extensions/Ingress:default/some-ingress",
|
|
},
|
|
},
|
|
})
|
|
ctrl := newFakeController(t.Context(), &fakeData{
|
|
apps: []runtime.Object{app, &defaultProj},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
|
kube.GetResourceKey(managedObj): managedObj,
|
|
kube.GetResourceKey(unmanagedObjWrongName): unmanagedObjWrongName,
|
|
kube.GetResourceKey(unmanagedObjWrongKind): unmanagedObjWrongKind,
|
|
kube.GetResourceKey(unmanagedObjWrongGroup): unmanagedObjWrongGroup,
|
|
kube.GetResourceKey(unmanagedObjWrongNamespace): unmanagedObjWrongNamespace,
|
|
},
|
|
}, nil)
|
|
|
|
manager := ctrl.appStateManager.(*appStateManager)
|
|
appName := "guestbook"
|
|
|
|
t.Run("will return true if trackingid matches the resource", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
configObj := managedObj.DeepCopy()
|
|
|
|
// then
|
|
assert.True(t, manager.isSelfReferencedObj(managedObj, configObj, appName, v1alpha1.TrackingMethodLabel, ""))
|
|
assert.True(t, manager.isSelfReferencedObj(managedObj, configObj, appName, v1alpha1.TrackingMethodAnnotation, ""))
|
|
})
|
|
t.Run("will return true if tracked with label", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
configObj := managedObjWithLabel.DeepCopy()
|
|
|
|
// then
|
|
assert.True(t, manager.isSelfReferencedObj(managedObjWithLabel, configObj, appName, v1alpha1.TrackingMethodLabel, ""))
|
|
})
|
|
t.Run("will handle if trackingId has wrong resource name and config is nil", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
|
|
// then
|
|
assert.True(t, manager.isSelfReferencedObj(unmanagedObjWrongName, nil, appName, v1alpha1.TrackingMethodLabel, ""))
|
|
assert.False(t, manager.isSelfReferencedObj(unmanagedObjWrongName, nil, appName, v1alpha1.TrackingMethodAnnotation, ""))
|
|
})
|
|
t.Run("will handle if trackingId has wrong resource group and config is nil", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
|
|
// then
|
|
assert.True(t, manager.isSelfReferencedObj(unmanagedObjWrongGroup, nil, appName, v1alpha1.TrackingMethodLabel, ""))
|
|
assert.False(t, manager.isSelfReferencedObj(unmanagedObjWrongGroup, nil, appName, v1alpha1.TrackingMethodAnnotation, ""))
|
|
})
|
|
t.Run("will handle if trackingId has wrong kind and config is nil", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
|
|
// then
|
|
assert.True(t, manager.isSelfReferencedObj(unmanagedObjWrongKind, nil, appName, v1alpha1.TrackingMethodLabel, ""))
|
|
assert.False(t, manager.isSelfReferencedObj(unmanagedObjWrongKind, nil, appName, v1alpha1.TrackingMethodAnnotation, ""))
|
|
})
|
|
t.Run("will handle if trackingId has wrong namespace and config is nil", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
|
|
// then
|
|
assert.True(t, manager.isSelfReferencedObj(unmanagedObjWrongNamespace, nil, appName, v1alpha1.TrackingMethodLabel, ""))
|
|
assert.False(t, manager.isSelfReferencedObj(unmanagedObjWrongNamespace, nil, appName, v1alpha1.TrackingMethodAnnotationAndLabel, ""))
|
|
})
|
|
t.Run("will return true if live is nil", func(t *testing.T) {
|
|
t.Parallel()
|
|
assert.True(t, manager.isSelfReferencedObj(nil, nil, appName, v1alpha1.TrackingMethodAnnotation, ""))
|
|
})
|
|
|
|
t.Run("will handle upgrade in desired state APIGroup", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
config := managedWrongAPIGroup.DeepCopy()
|
|
delete(config.GetAnnotations(), common.AnnotationKeyAppInstance)
|
|
|
|
// then
|
|
assert.True(t, manager.isSelfReferencedObj(managedWrongAPIGroup, config, appName, v1alpha1.TrackingMethodAnnotation, ""))
|
|
})
|
|
}
|
|
|
|
func TestUseDiffCache(t *testing.T) {
|
|
t.Parallel()
|
|
type fixture struct {
|
|
testName string
|
|
noCache bool
|
|
manifestInfos []*apiclient.ManifestResponse
|
|
sources []v1alpha1.ApplicationSource
|
|
app *v1alpha1.Application
|
|
manifestRevisions []string
|
|
statusRefreshTimeout time.Duration
|
|
expectedUseCache bool
|
|
serverSideDiff 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",
|
|
},
|
|
}
|
|
}
|
|
source := func() v1alpha1.ApplicationSource {
|
|
return v1alpha1.ApplicationSource{
|
|
RepoURL: "https://some-repo.com",
|
|
Path: "argocd/httpbin",
|
|
TargetRevision: "HEAD",
|
|
}
|
|
}
|
|
sources := func() []v1alpha1.ApplicationSource {
|
|
return []v1alpha1.ApplicationSource{source()}
|
|
}
|
|
|
|
app := func(namespace string, revision string, refresh bool, a *v1alpha1.Application) *v1alpha1.Application {
|
|
app := &v1alpha1.Application{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "httpbin",
|
|
Namespace: namespace,
|
|
},
|
|
Spec: v1alpha1.ApplicationSpec{
|
|
Source: new(source()),
|
|
Destination: v1alpha1.ApplicationDestination{
|
|
Server: "https://kubernetes.default.svc",
|
|
Namespace: "httpbin",
|
|
},
|
|
Project: "default",
|
|
SyncPolicy: &v1alpha1.SyncPolicy{
|
|
SyncOptions: []string{
|
|
"CreateNamespace=true",
|
|
"ServerSideApply=true",
|
|
},
|
|
},
|
|
},
|
|
Status: v1alpha1.ApplicationStatus{
|
|
Resources: []v1alpha1.ResourceStatus{},
|
|
Sync: v1alpha1.SyncStatus{
|
|
Status: v1alpha1.SyncStatusCodeSynced,
|
|
ComparedTo: v1alpha1.ComparedTo{
|
|
Source: source(),
|
|
Destination: v1alpha1.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[v1alpha1.AnnotationKeyRefresh] = string(v1alpha1.RefreshTypeNormal)
|
|
app.SetAnnotations(annotations)
|
|
}
|
|
if a != nil {
|
|
err := mergo.Merge(app, a, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue)
|
|
require.NoErrorf(t, err, "error merging app")
|
|
}
|
|
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,
|
|
serverSideDiff: false,
|
|
},
|
|
{
|
|
testName: "will use diff cache with sync policy",
|
|
noCache: false,
|
|
manifestInfos: manifestInfos("rev1"),
|
|
sources: []v1alpha1.ApplicationSource{test.YamlToApplication(testdata.DiffCacheYaml).Status.Sync.ComparedTo.Source},
|
|
app: test.YamlToApplication(testdata.DiffCacheYaml),
|
|
manifestRevisions: []string{"rev1"},
|
|
statusRefreshTimeout: time.Hour * 24,
|
|
expectedUseCache: true,
|
|
serverSideDiff: true,
|
|
},
|
|
{
|
|
testName: "will use diff cache for multisource",
|
|
noCache: false,
|
|
manifestInfos: append(manifestInfos("rev1"), manifestInfos("rev2")...),
|
|
sources: v1alpha1.ApplicationSources{
|
|
{
|
|
RepoURL: "multisource repo1",
|
|
},
|
|
{
|
|
RepoURL: "multisource repo2",
|
|
},
|
|
},
|
|
app: app("httpbin", "", false, &v1alpha1.Application{
|
|
Spec: v1alpha1.ApplicationSpec{
|
|
Source: nil,
|
|
Sources: v1alpha1.ApplicationSources{
|
|
{
|
|
RepoURL: "multisource repo1",
|
|
},
|
|
{
|
|
RepoURL: "multisource repo2",
|
|
},
|
|
},
|
|
},
|
|
Status: v1alpha1.ApplicationStatus{
|
|
Resources: []v1alpha1.ResourceStatus{},
|
|
Sync: v1alpha1.SyncStatus{
|
|
Status: v1alpha1.SyncStatusCodeSynced,
|
|
ComparedTo: v1alpha1.ComparedTo{
|
|
Source: v1alpha1.ApplicationSource{},
|
|
Sources: v1alpha1.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,
|
|
serverSideDiff: false,
|
|
},
|
|
{
|
|
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,
|
|
serverSideDiff: 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,
|
|
serverSideDiff: 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,
|
|
serverSideDiff: false,
|
|
},
|
|
{
|
|
testName: "will return true if status expired and server-side diff",
|
|
noCache: false,
|
|
manifestInfos: manifestInfos("rev1"),
|
|
sources: sources(),
|
|
app: app("httpbin", "rev1", false, nil),
|
|
manifestRevisions: []string{"rev1"},
|
|
statusRefreshTimeout: time.Minute,
|
|
expectedUseCache: true,
|
|
serverSideDiff: true,
|
|
},
|
|
{
|
|
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,
|
|
serverSideDiff: false,
|
|
},
|
|
{
|
|
testName: "will return false if app spec repo changed",
|
|
noCache: false,
|
|
manifestInfos: manifestInfos("rev1"),
|
|
sources: sources(),
|
|
app: app("httpbin", "rev1", false, &v1alpha1.Application{
|
|
Spec: v1alpha1.ApplicationSpec{
|
|
Source: &v1alpha1.ApplicationSource{
|
|
RepoURL: "new-repo",
|
|
},
|
|
},
|
|
}),
|
|
manifestRevisions: []string{"rev1"},
|
|
statusRefreshTimeout: time.Hour * 24,
|
|
expectedUseCache: false,
|
|
serverSideDiff: false,
|
|
},
|
|
{
|
|
testName: "will return false if app spec IgnoreDifferences changed",
|
|
noCache: false,
|
|
manifestInfos: manifestInfos("rev1"),
|
|
sources: sources(),
|
|
app: app("httpbin", "rev1", false, &v1alpha1.Application{
|
|
Spec: v1alpha1.ApplicationSpec{
|
|
IgnoreDifferences: []v1alpha1.ResourceIgnoreDifferences{
|
|
{
|
|
Group: "app/v1",
|
|
Kind: "application",
|
|
Name: "httpbin",
|
|
Namespace: "httpbin",
|
|
JQPathExpressions: []string{"."},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
manifestRevisions: []string{"rev1"},
|
|
statusRefreshTimeout: time.Hour * 24,
|
|
expectedUseCache: false,
|
|
serverSideDiff: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
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, tc.serverSideDiff, log)
|
|
// Then
|
|
assert.Equal(t, tc.expectedUseCache, useDiffCache)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompareAppStateDefaultRevisionUpdated(t *testing.T) {
|
|
app := newFakeApp()
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.True(t, compRes.revisionsMayHaveChanges)
|
|
}
|
|
|
|
func TestCompareAppStateRevisionUpdatedWithHelmSource(t *testing.T) {
|
|
app := newFakeMultiSourceApp()
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(t.Context(), &data, nil)
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, app.Spec.GetSource())
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "")
|
|
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, compRes)
|
|
assert.NotNil(t, compRes.syncStatus)
|
|
assert.True(t, compRes.revisionsMayHaveChanges)
|
|
}
|
|
|
|
func Test_normalizeClusterScopeTracking(t *testing.T) {
|
|
obj := kube.MustToUnstructured(&rbacv1.ClusterRole{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test",
|
|
Namespace: "test",
|
|
},
|
|
})
|
|
c := &cachemocks.ClusterCache{}
|
|
c.EXPECT().IsNamespaced(mock.Anything).Return(false, nil)
|
|
var called bool
|
|
err := normalizeClusterScopeTracking([]*unstructured.Unstructured{obj}, c, func(u *unstructured.Unstructured) error {
|
|
// We expect that the normalization function will call this callback with an obj that has had the namespace set
|
|
// to empty.
|
|
called = true
|
|
assert.Empty(t, u.GetNamespace())
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, called, "normalization function should have called the callback function")
|
|
}
|
|
|
|
func TestCompareAppState_CallUpdateRevisionForPaths_ForOCI(t *testing.T) {
|
|
app := newFakeApp()
|
|
// Enable the manifest-generate-paths annotation and set a synced revision
|
|
app.SetAnnotations(map[string]string{v1alpha1.AnnotationKeyManifestGeneratePaths: "."})
|
|
app.Status.Sync = v1alpha1.SyncStatus{
|
|
Revision: "abc123",
|
|
Status: v1alpha1.SyncStatusCodeSynced,
|
|
}
|
|
|
|
data := fakeData{
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
updateRevisionForPathsResponse: &apiclient.UpdateRevisionForPathsResponse{Changes: false},
|
|
}
|
|
ctrl := newFakeControllerWithResync(t.Context(), &data, time.Minute, nil, nil)
|
|
|
|
source := app.Spec.GetSource()
|
|
source.RepoURL = "oci://example.com/argo/argo-cd"
|
|
sources := make([]v1alpha1.ApplicationSource, 0)
|
|
sources = append(sources, source)
|
|
|
|
_, _, revisionsMayHaveChanges, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, sources, "abc123", []string{"123456"}, false, false, defaultProj.EffectiveSourceIntegrity(), &defaultProj, false)
|
|
require.NoError(t, err)
|
|
require.False(t, revisionsMayHaveChanges)
|
|
}
|
|
|
|
func TestCompareAppState_CallUpdateRevisionForPaths_ForMultiSource(t *testing.T) {
|
|
app := newFakeApp()
|
|
// Enable the manifest-generate-paths annotation and set a synced revision
|
|
app.SetAnnotations(map[string]string{v1alpha1.AnnotationKeyManifestGeneratePaths: "."})
|
|
app.Status.Sync = v1alpha1.SyncStatus{
|
|
Revision: "abc123",
|
|
Status: v1alpha1.SyncStatusCodeSynced,
|
|
Revisions: []string{"0.0.1", "resolved-abc123", "resolved-main"},
|
|
}
|
|
|
|
app.Spec.Sources = v1alpha1.ApplicationSources{
|
|
{RepoURL: "oci://example.com/argo/argo-cd", TargetRevision: "0.0.1", Helm: &v1alpha1.ApplicationSourceHelm{ValueFiles: []string{"$values/my-path"}}},
|
|
{Ref: "values", RepoURL: "https://git.test.com", TargetRevision: "abc123"},
|
|
{TargetRevision: "main", RepoURL: "https://git.test.com", Path: "path/to/chart"},
|
|
}
|
|
|
|
data := fakeData{
|
|
manifestResponses: []*apiclient.ManifestResponse{
|
|
{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "0.0.1",
|
|
},
|
|
{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "main",
|
|
},
|
|
},
|
|
updateRevisionForPathsResponses: []*apiclient.UpdateRevisionForPathsResponse{
|
|
{Changes: false, Revision: "0.0.1"},
|
|
{Changes: false, Revision: "resolved-main"},
|
|
},
|
|
}
|
|
ctrl := newFakeControllerWithResync(t.Context(), &data, time.Minute, nil, nil)
|
|
|
|
revisions := make([]string, 0)
|
|
revisions = append(revisions, "0.0.1", "abc123", "main")
|
|
|
|
sources := app.Spec.Sources
|
|
|
|
_, _, revisionsMayHaveChanges, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, sources, "0.0.1", revisions, false, false, defaultProj.EffectiveSourceIntegrity(), &defaultProj, false)
|
|
require.NoError(t, err)
|
|
require.False(t, revisionsMayHaveChanges)
|
|
}
|
|
|
|
func Test_GetRepoObjs_HydrateToAppPathNotExist(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("with hydrateTo: appends waiting message", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := newFakeApp()
|
|
app.Spec.Source = nil
|
|
app.Spec.SourceHydrator = &v1alpha1.SourceHydrator{
|
|
DrySource: v1alpha1.DrySource{
|
|
RepoURL: "https://github.com/example/repo",
|
|
TargetRevision: "main",
|
|
Path: "apps/my-app",
|
|
},
|
|
SyncSource: v1alpha1.SyncSource{
|
|
TargetBranch: "env/prod",
|
|
Path: "env/prod/my-app",
|
|
},
|
|
HydrateTo: &v1alpha1.HydrateTo{
|
|
TargetBranch: "env/prod-next",
|
|
},
|
|
}
|
|
|
|
ctrl := newFakeController(t.Context(), &fakeData{manifestResponse: &apiclient.ManifestResponse{}}, errors.New("env/prod/my-app: app path does not exist"))
|
|
source := app.Spec.GetSource()
|
|
|
|
_, _, _, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, []v1alpha1.ApplicationSource{source}, "app", []string{""}, true, false, nil, &defaultProj, false)
|
|
require.ErrorContains(t, err, "app path does not exist")
|
|
require.ErrorContains(t, err, "waiting for an external process to update env/prod from env/prod-next")
|
|
})
|
|
t.Run("without hydrateTo: no waiting message appended", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := newFakeApp()
|
|
app.Spec.Source = nil
|
|
app.Spec.SourceHydrator = &v1alpha1.SourceHydrator{
|
|
DrySource: v1alpha1.DrySource{
|
|
RepoURL: "https://github.com/example/repo",
|
|
TargetRevision: "main",
|
|
Path: "apps/my-app",
|
|
},
|
|
SyncSource: v1alpha1.SyncSource{
|
|
TargetBranch: "env/prod",
|
|
Path: "env/prod/my-app",
|
|
},
|
|
}
|
|
|
|
ctrl := newFakeController(t.Context(), &fakeData{manifestResponse: &apiclient.ManifestResponse{}}, errors.New("env/prod/my-app: app path does not exist"))
|
|
source := app.Spec.GetSource()
|
|
|
|
_, _, _, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, []v1alpha1.ApplicationSource{source}, "app", []string{""}, true, false, nil, &defaultProj, false)
|
|
require.ErrorContains(t, err, "app path does not exist")
|
|
require.NotContains(t, err.Error(), "waiting for an external process")
|
|
})
|
|
}
|
|
|
|
func Test_isObjRequiresDeletionConfirmation(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
resourceSyncOptions []string
|
|
appSyncOptions []string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "default",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "confirm delete resource",
|
|
resourceSyncOptions: []string{"Delete=confirm"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "confirm delete app",
|
|
appSyncOptions: []string{"Delete=confirm"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "confirm prune resource",
|
|
appSyncOptions: []string{"Prune=confirm"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "confirm app & resource delete",
|
|
appSyncOptions: []string{"Delete=confirm"},
|
|
resourceSyncOptions: []string{"Delete=confirm"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "confirm app & resource override",
|
|
appSyncOptions: []string{"Delete=confirm"},
|
|
resourceSyncOptions: []string{"Delete=foo"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "confirm app & resource mixed delete and prune",
|
|
appSyncOptions: []string{"Prune=confirm"},
|
|
resourceSyncOptions: []string{"Delete=confirm"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "override prune resource",
|
|
appSyncOptions: []string{"Prune=confirm"},
|
|
resourceSyncOptions: []string{"Prune=foo"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "override delete resource and additional delete confirm",
|
|
appSyncOptions: []string{"Delete=confirm", "Prune=confirm"},
|
|
resourceSyncOptions: []string{"Delete=foo"},
|
|
expected: true,
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
obj := NewPod()
|
|
obj.SetAnnotations(map[string]string{"argocd.argoproj.io/sync-options": strings.Join(tt.resourceSyncOptions, ",")})
|
|
|
|
app := newFakeApp()
|
|
app.Spec.SyncPolicy.SyncOptions = tt.appSyncOptions
|
|
|
|
require.Equal(t, tt.expected, isObjRequiresDeletionConfirmation(obj, app))
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_evaluateRevisionChanges(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
source *v1alpha1.ApplicationSource
|
|
sourceType v1alpha1.ApplicationSourceType
|
|
syncPolicy *v1alpha1.SyncPolicy
|
|
revision string
|
|
appSyncedRevision string
|
|
refSources map[string]*v1alpha1.RefTarget
|
|
repoDepth int64
|
|
keyManifestGenerateAnnotationExists bool
|
|
keyManifestGenerateAnnotationVal string
|
|
updateRevisionForPathsResponse *apiclient.UpdateRevisionForPathsResponse
|
|
expectedRevision string
|
|
expectedHasChanges bool
|
|
expectUpdateRevisionForPathsCalled bool
|
|
}{
|
|
{
|
|
name: "Ref source returns early with no changes",
|
|
source: &v1alpha1.ApplicationSource{
|
|
RepoURL: "https://github.com/example/repo",
|
|
Ref: "main",
|
|
},
|
|
sourceType: v1alpha1.ApplicationSourceTypeHelm,
|
|
revision: "abc123",
|
|
appSyncedRevision: "def456",
|
|
expectedRevision: "abc123",
|
|
expectedHasChanges: false,
|
|
},
|
|
{
|
|
name: "Same revision with no ref sources returns early",
|
|
source: &v1alpha1.ApplicationSource{
|
|
RepoURL: "https://github.com/example/repo",
|
|
Path: "manifests",
|
|
},
|
|
sourceType: v1alpha1.ApplicationSourceTypeKustomize,
|
|
revision: "abc123",
|
|
appSyncedRevision: "abc123",
|
|
refSources: map[string]*v1alpha1.RefTarget{},
|
|
expectedRevision: "abc123",
|
|
expectedHasChanges: false,
|
|
},
|
|
{
|
|
name: "Same revision with ref sources continues to evaluation",
|
|
source: &v1alpha1.ApplicationSource{
|
|
RepoURL: "https://github.com/example/repo",
|
|
Path: "manifests",
|
|
},
|
|
sourceType: v1alpha1.ApplicationSourceTypeKustomize,
|
|
revision: "abc123",
|
|
appSyncedRevision: "abc123",
|
|
refSources: map[string]*v1alpha1.RefTarget{
|
|
"ref1": {Repo: v1alpha1.Repository{Repo: "https://github.com/example/ref"}},
|
|
},
|
|
repoDepth: 0,
|
|
keyManifestGenerateAnnotationExists: true,
|
|
keyManifestGenerateAnnotationVal: ".",
|
|
updateRevisionForPathsResponse: &apiclient.UpdateRevisionForPathsResponse{
|
|
Revision: "abc123",
|
|
Changes: false,
|
|
},
|
|
expectedRevision: "abc123",
|
|
expectedHasChanges: false,
|
|
expectUpdateRevisionForPathsCalled: true,
|
|
},
|
|
{
|
|
name: "Shallow clone skips UpdateRevisionForPaths",
|
|
source: &v1alpha1.ApplicationSource{
|
|
RepoURL: "https://github.com/example/repo",
|
|
Path: "manifests",
|
|
},
|
|
sourceType: v1alpha1.ApplicationSourceTypeKustomize,
|
|
syncPolicy: &v1alpha1.SyncPolicy{
|
|
Automated: &v1alpha1.SyncPolicyAutomated{},
|
|
},
|
|
revision: "abc123",
|
|
appSyncedRevision: "def456",
|
|
repoDepth: 1,
|
|
keyManifestGenerateAnnotationExists: true,
|
|
keyManifestGenerateAnnotationVal: ".",
|
|
expectedRevision: "abc123",
|
|
expectedHasChanges: true,
|
|
expectUpdateRevisionForPathsCalled: false,
|
|
},
|
|
{
|
|
name: "Missing annotation skips UpdateRevisionForPaths",
|
|
source: &v1alpha1.ApplicationSource{
|
|
RepoURL: "https://github.com/example/repo",
|
|
Path: "manifests",
|
|
},
|
|
sourceType: v1alpha1.ApplicationSourceTypeKustomize,
|
|
syncPolicy: &v1alpha1.SyncPolicy{
|
|
Automated: &v1alpha1.SyncPolicyAutomated{},
|
|
},
|
|
revision: "abc123",
|
|
appSyncedRevision: "def456",
|
|
repoDepth: 0,
|
|
keyManifestGenerateAnnotationExists: false,
|
|
keyManifestGenerateAnnotationVal: "",
|
|
expectedRevision: "abc123",
|
|
expectedHasChanges: true,
|
|
expectUpdateRevisionForPathsCalled: false,
|
|
},
|
|
{
|
|
name: "UpdateRevisionForPaths returns updated revision",
|
|
source: &v1alpha1.ApplicationSource{
|
|
RepoURL: "https://github.com/example/repo",
|
|
Path: "manifests",
|
|
},
|
|
sourceType: v1alpha1.ApplicationSourceTypeKustomize,
|
|
syncPolicy: &v1alpha1.SyncPolicy{
|
|
Automated: &v1alpha1.SyncPolicyAutomated{},
|
|
},
|
|
revision: "HEAD",
|
|
appSyncedRevision: "def456",
|
|
repoDepth: 0,
|
|
keyManifestGenerateAnnotationExists: true,
|
|
keyManifestGenerateAnnotationVal: ".",
|
|
updateRevisionForPathsResponse: &apiclient.UpdateRevisionForPathsResponse{
|
|
Revision: "abc123resolved",
|
|
Changes: true,
|
|
},
|
|
expectedRevision: "abc123resolved",
|
|
expectedHasChanges: true,
|
|
expectUpdateRevisionForPathsCalled: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
app := newFakeApp()
|
|
app.Spec.SyncPolicy = tt.syncPolicy
|
|
app.Status.Sync.Revision = tt.appSyncedRevision
|
|
app.Status.SourceType = tt.sourceType
|
|
if tt.keyManifestGenerateAnnotationExists {
|
|
app.Annotations = map[string]string{
|
|
v1alpha1.AnnotationKeyManifestGeneratePaths: tt.keyManifestGenerateAnnotationVal,
|
|
}
|
|
}
|
|
|
|
repo := &v1alpha1.Repository{
|
|
Repo: tt.source.RepoURL,
|
|
Depth: tt.repoDepth,
|
|
}
|
|
|
|
mockRepoClient := &mocks.RepoServerServiceClient{}
|
|
if tt.expectUpdateRevisionForPathsCalled {
|
|
mockRepoClient.On("UpdateRevisionForPaths", mock.Anything, mock.Anything).Return(tt.updateRevisionForPathsResponse, nil)
|
|
}
|
|
|
|
mgr := &appStateManager{
|
|
namespace: "test-namespace",
|
|
}
|
|
|
|
resolvedRevision, hasChanges, err := mgr.evaluateRevisionChanges(
|
|
context.Background(),
|
|
mockRepoClient,
|
|
app,
|
|
tt.source,
|
|
0, // sourceIndex
|
|
repo,
|
|
tt.revision,
|
|
tt.refSources,
|
|
nil,
|
|
false,
|
|
"app.kubernetes.io/instance",
|
|
"v1.28.0",
|
|
[]string{"v1"},
|
|
"label",
|
|
"test-installation",
|
|
tt.keyManifestGenerateAnnotationExists,
|
|
tt.keyManifestGenerateAnnotationVal,
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.expectedRevision, resolvedRevision)
|
|
assert.Equal(t, tt.expectedHasChanges, hasChanges)
|
|
|
|
if tt.expectUpdateRevisionForPathsCalled {
|
|
mockRepoClient.AssertExpectations(t)
|
|
} else {
|
|
mockRepoClient.AssertNotCalled(t, "UpdateRevisionForPaths")
|
|
}
|
|
})
|
|
}
|
|
}
|