argo-cd/cmd/argocd/commands/applicationset_test.go
Peter Jiang 19b41b9d31
feat: ApplicationSet watch API (#26409)
Signed-off-by: nitishfy <justnitish06@gmail.com>
Signed-off-by: Peter Jiang <peterjiang823@gmail.com>
Co-authored-by: nitishfy <justnitish06@gmail.com>
2026-02-26 10:07:00 -05:00

375 lines
10 KiB
Go

package commands
import (
"context"
"errors"
"io"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
)
// TestAppSetDeleteWaitFlow verifies that when --wait is used and the appset has
// finalizers (still exists after Delete), the delete command watches for Deleted.
func TestAppSetDeleteWaitFlow(t *testing.T) {
appSetEventsCh := make(chan *v1alpha1.ApplicationSetWatchEvent, 1)
go func() {
defer close(appSetEventsCh)
appSetEventsCh <- &v1alpha1.ApplicationSetWatchEvent{
Type: watch.Added,
ApplicationSet: v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{Name: "test-appset"}},
}
appSetEventsCh <- &v1alpha1.ApplicationSetWatchEvent{Type: watch.Deleted}
}()
receivedDeleted := false
for appEvent := range appSetEventsCh {
if appEvent != nil && appEvent.Type == watch.Deleted {
receivedDeleted = true
break
}
}
assert.True(t, receivedDeleted, "wait loop should receive Deleted event from watch")
}
// TestAppSetCreateWaitFlow verifies that when --wait is used, the create command
// waits for ResourcesUpToDate from the watch before completing.
func TestAppSetCreateWaitFlow(t *testing.T) {
fakeClient := &fakeAcdClient{}
ctx := context.Background()
err := waitForApplicationSetResourcesUpToDate(ctx, fakeClient, "test-appset")
require.NoError(t, err)
}
func TestAppSetCreateWaitDeletedError(t *testing.T) {
appSetEventsCh := make(chan *v1alpha1.ApplicationSetWatchEvent, 1)
go func() {
defer close(appSetEventsCh)
appSetEventsCh <- &v1alpha1.ApplicationSetWatchEvent{Type: watch.Deleted}
}()
var err error
for appEvent := range appSetEventsCh {
if appEvent == nil {
continue
}
if appEvent.Type == watch.Deleted {
err = errors.New("ApplicationSet was deleted before reaching ResourcesUpToDate")
break
}
}
require.Error(t, err)
assert.Contains(t, err.Error(), "deleted before reaching ResourcesUpToDate")
}
func TestIsApplicationSetResourcesUpToDate(t *testing.T) {
t.Run("returns true when ResourcesUpToDate is True", func(t *testing.T) {
appSet := &v1alpha1.ApplicationSet{
Status: v1alpha1.ApplicationSetStatus{
Conditions: []v1alpha1.ApplicationSetCondition{
{Type: v1alpha1.ApplicationSetConditionResourcesUpToDate, Status: v1alpha1.ApplicationSetConditionStatusTrue},
},
},
}
assert.True(t, isApplicationSetResourcesUpToDate(appSet))
})
t.Run("returns false when ResourcesUpToDate is False", func(t *testing.T) {
appSet := &v1alpha1.ApplicationSet{
Status: v1alpha1.ApplicationSetStatus{
Conditions: []v1alpha1.ApplicationSetCondition{
{Type: v1alpha1.ApplicationSetConditionResourcesUpToDate, Status: v1alpha1.ApplicationSetConditionStatusFalse},
},
},
}
assert.False(t, isApplicationSetResourcesUpToDate(appSet))
})
t.Run("returns false when no conditions", func(t *testing.T) {
appSet := &v1alpha1.ApplicationSet{}
assert.False(t, isApplicationSetResourcesUpToDate(appSet))
})
}
func TestPrintApplicationSetNames(t *testing.T) {
output, _ := captureOutput(func() error {
appSet := &v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
}
appSet2 := &v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Namespace: "team-one",
Name: "test",
},
}
printApplicationSetNames([]v1alpha1.ApplicationSet{*appSet, *appSet2})
return nil
})
expectation := "test\nteam-one/test\n"
require.Equalf(t, output, expectation, "Incorrect print params output %q, should be %q", output, expectation)
}
func TestPrintApplicationSetTable(t *testing.T) {
output, err := captureOutput(func() error {
app := &v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "app-name",
},
Spec: v1alpha1.ApplicationSetSpec{
Generators: []v1alpha1.ApplicationSetGenerator{
{
Git: &v1alpha1.GitGenerator{
RepoURL: "https://github.com/argoproj/argo-cd.git",
Revision: "head",
Directories: []v1alpha1.GitDirectoryGeneratorItem{
{
Path: "applicationset/examples/git-generator-directory/cluster-addons/*",
},
},
},
},
},
Template: v1alpha1.ApplicationSetTemplate{
Spec: v1alpha1.ApplicationSpec{
Project: "default",
},
},
},
Status: v1alpha1.ApplicationSetStatus{
Conditions: []v1alpha1.ApplicationSetCondition{
{
Status: v1alpha1.ApplicationSetConditionStatusTrue,
Type: v1alpha1.ApplicationSetConditionResourcesUpToDate,
},
},
},
}
app2 := &v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "app-name",
Namespace: "team-two",
},
Spec: v1alpha1.ApplicationSetSpec{
Generators: []v1alpha1.ApplicationSetGenerator{
{
Git: &v1alpha1.GitGenerator{
RepoURL: "https://github.com/argoproj/argo-cd.git",
Revision: "head",
Directories: []v1alpha1.GitDirectoryGeneratorItem{
{
Path: "applicationset/examples/git-generator-directory/cluster-addons/*",
},
},
},
},
},
Template: v1alpha1.ApplicationSetTemplate{
Spec: v1alpha1.ApplicationSpec{
Project: "default",
},
},
},
Status: v1alpha1.ApplicationSetStatus{
Conditions: []v1alpha1.ApplicationSetCondition{
{
Status: v1alpha1.ApplicationSetConditionStatusTrue,
Type: v1alpha1.ApplicationSetConditionResourcesUpToDate,
},
},
},
}
output := "table"
printApplicationSetTable([]v1alpha1.ApplicationSet{*app, *app2}, &output)
return nil
})
require.NoError(t, err)
expectation := "NAME PROJECT SYNCPOLICY HEALTH CONDITIONS\napp-name default nil [{ResourcesUpToDate <nil> True }]\nteam-two/app-name default nil [{ResourcesUpToDate <nil> True }]\n"
assert.Equal(t, expectation, output)
}
func TestPrintAppSetSummaryTable(t *testing.T) {
baseAppSet := &v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "app-name",
},
Spec: v1alpha1.ApplicationSetSpec{
Generators: []v1alpha1.ApplicationSetGenerator{
{
Git: &v1alpha1.GitGenerator{
RepoURL: "https://github.com/argoproj/argo-cd.git",
Revision: "head",
Directories: []v1alpha1.GitDirectoryGeneratorItem{
{
Path: "applicationset/examples/git-generator-directory/cluster-addons/*",
},
},
},
},
},
Template: v1alpha1.ApplicationSetTemplate{
Spec: v1alpha1.ApplicationSpec{
Project: "default",
},
},
},
Status: v1alpha1.ApplicationSetStatus{
Conditions: []v1alpha1.ApplicationSetCondition{
{
Status: v1alpha1.ApplicationSetConditionStatusTrue,
Type: v1alpha1.ApplicationSetConditionResourcesUpToDate,
},
},
},
}
appsetSpecSource := baseAppSet.DeepCopy()
appsetSpecSource.Spec.Template.Spec.Source = &v1alpha1.ApplicationSource{
RepoURL: "test1",
TargetRevision: "master1",
Path: "/test1",
}
appsetSpecSources := baseAppSet.DeepCopy()
appsetSpecSources.Spec.Template.Spec.Sources = v1alpha1.ApplicationSources{
{
RepoURL: "test1",
TargetRevision: "master1",
Path: "/test1",
},
{
RepoURL: "test2",
TargetRevision: "master2",
Path: "/test2",
},
}
appsetSpecSyncPolicy := baseAppSet.DeepCopy()
appsetSpecSyncPolicy.Spec.SyncPolicy = &v1alpha1.ApplicationSetSyncPolicy{
PreserveResourcesOnDeletion: true,
}
appSetTemplateSpecSyncPolicy := baseAppSet.DeepCopy()
appSetTemplateSpecSyncPolicy.Spec.Template.Spec.SyncPolicy = &v1alpha1.SyncPolicy{
Automated: &v1alpha1.SyncPolicyAutomated{
SelfHeal: true,
},
}
appSetBothSyncPolicies := baseAppSet.DeepCopy()
appSetBothSyncPolicies.Spec.SyncPolicy = &v1alpha1.ApplicationSetSyncPolicy{
PreserveResourcesOnDeletion: true,
}
appSetBothSyncPolicies.Spec.Template.Spec.SyncPolicy = &v1alpha1.SyncPolicy{
Automated: &v1alpha1.SyncPolicyAutomated{
SelfHeal: true,
},
}
for _, tt := range []struct {
name string
appSet *v1alpha1.ApplicationSet
expectedOutput string
}{
{
name: "appset with only spec.syncPolicy set",
appSet: appsetSpecSyncPolicy,
expectedOutput: `Name: app-name
Project: default
Server:
Namespace:
Health Status:
Source:
- Repo:
Target:
SyncPolicy: <none>
`,
},
{
name: "appset with only spec.template.spec.syncPolicy set",
appSet: appSetTemplateSpecSyncPolicy,
expectedOutput: `Name: app-name
Project: default
Server:
Namespace:
Health Status:
Source:
- Repo:
Target:
SyncPolicy: Automated
`,
},
{
name: "appset with both spec.SyncPolicy and spec.template.spec.syncPolicy set",
appSet: appSetBothSyncPolicies,
expectedOutput: `Name: app-name
Project: default
Server:
Namespace:
Health Status:
Source:
- Repo:
Target:
SyncPolicy: Automated
`,
},
{
name: "appset with a single source",
appSet: appsetSpecSource,
expectedOutput: `Name: app-name
Project: default
Server:
Namespace:
Health Status:
Source:
- Repo: test1
Target: master1
Path: /test1
SyncPolicy: <none>
`,
},
{
name: "appset with a multiple sources",
appSet: appsetSpecSources,
expectedOutput: `Name: app-name
Project: default
Server:
Namespace:
Health Status:
Sources:
- Repo: test1
Target: master1
Path: /test1
- Repo: test2
Target: master2
Path: /test2
SyncPolicy: <none>
`,
},
} {
t.Run(tt.name, func(t *testing.T) {
oldStdout := os.Stdout
defer func() {
os.Stdout = oldStdout
}()
r, w, _ := os.Pipe()
os.Stdout = w
printAppSetSummaryTable(tt.appSet)
w.Close()
out, err := io.ReadAll(r)
require.NoError(t, err)
assert.Equal(t, tt.expectedOutput, string(out))
})
}
}