mirror of
https://github.com/hanzoai/deploy.git
synced 2026-10-07 08:44:50 +00:00
* feat(api): hard-fork CRD API group argoproj.io -> deploy.hanzo.ai
Hanzo CD hard-fork, phase 1.1. Single source of truth is
pkg/apis/application/register.go's Group constant (Application/AppProject/
ApplicationSet all derive their FullName from it); every other Go call-site
that hardcoded the literal group string is updated to match: the 3 generated
informer GVRs, the app-of-apps nested-Application detector in types.go, the
resource-tree Application-kind cache populator, the 3 NotFound error
constructors in server/application, and the applicationset-controller's
leader-election Lease name/ID (kept in lockstep across Go source + RBAC
resourceNames, or leader election would get RBAC-denied on startup).
CRD YAMLs (application/appproject/applicationset) and every RBAC apiGroups
rule that actually governs our 3 CRDs are repointed to deploy.hanzo.ai, across
both manifests/base/* (namespaced Roles) and manifests/cluster-rbac/* (cluster
scope). Two adjacent things that share the "argoproj.io" string but are NOT
ours are deliberately left untouched: gitops-engine's health-check dispatch
for Argo Workflows' `Workflow` kind, and argocd-server-clusterrole's second
apiGroups block granting read on Argo Workflows' `workflows` resource — both
belong to a separate, unrelated Argoproj-family project we are not forking.
Also untouched, per explicit scope: annotation/label keys (argocd.argoproj.io/*)
and finalizers (*.argoproj.io) that track already-applied live resources, and
argocd-cm.yaml's resource.customizations.ignoreResourceUpdates keys (a narrow
app-of-apps diffing nicety, not build- or RBAC-relevant).
CGO_ENABLED=0 go build ./... verified green.
* ci: wire canonical hanzo.yml + cicd.yml (Hanzo CD image build)
Phase 1.3. Follows the same one-way pattern every hanzo/lux/zoo repo uses:
hanzo.yml declares the image (root Dockerfile -> ghcr.io/hanzoai/deploy) and a
fast go-build sanity test (the multi-stage Dockerfile build is the slow, real
gate); cicd.yml is the ~10-line trigger shim importing hanzoai/ci's reusable
build workflow on the arc pool, secrets from KMS only.
No `deploy:` block yet, matching the hanzoai/cms build-only-first-cutover
pattern: this repo's own live hanzo-cd namespace runs the whole Hanzo fleet's
GitOps, so the prod image/CRD swap is a deliberate separate action (see the
[HELD] universe PR), never an automatic rollout on merge. Every PR/push still
builds and pushes an immutable ghcr.io/hanzoai/deploy:sha-<7>-amd64 image.
* feat(ui): rebrand Argo CD -> Hanzo CD (title, favicon, logo, sidebar)
Phase 1.2. User-facing "Argo CD"/"ArgoCD" text replaced with "Hanzo CD"
across the login page, help modal, settings screens, version-info panel,
sync-options warning, fullscreen-logs title, app.tsx page titles, and
index.html's <title>/noscript text (dropped the now-wrong upstream CLI
doc link rather than invent a hanzo.ai URL that doesn't exist).
Logo/favicon assets replaced with the canonical Hanzo mark (never
hand-drawn, copied from ~/work/hanzo/logo/dist): the two real usages of
Argo's logo collapse onto one Hanzo glyph (mono + white variants) at the
same 5 call sites (login icon + footer, login.scss $logoPath/
$logoNegativePath, sidebar text-logo), preserving the existing CSS
invert() filters so both the light login panel and dark sidebar still
render correctly. Old argologo.svg/argo_o.svg/argo.png deleted (no
longer referenced). favicon-{16x16,32x32}.png added at
ui/src/assets/favicon/ (index.html's links to that path were previously
dead - the directory didn't exist in this checkout). package.json name
argo-cd-ui -> hanzo-cd-ui.
Left alone, deliberately: the two Kubernetes resource-kind icon sets
under ui/src/assets/images/resources/ (unrelated to branding), and
upstream doc/project links (argoproj.io, argo-cd.readthedocs.io) since
we have no equivalent hanzo.ai docs to point to yet - except the login
footer logo's outbound link, which now points to hanzo.ai instead of
argoproj.io (a Hanzo-branded mark linking to the upstream project it
was forked from read as an obvious leftover).
* feat(module): rename Go module github.com/argoproj/argo-cd/v3 -> github.com/hanzoai/deploy
Phase 2. No version suffix on the new path (org policy: never publish our
own Go modules above v1.x.x / with a v2+ suffix) — this fork starts a fresh
v0/v1 module identity rather than carrying the v3 the upstream fork point
had reached.
Mechanical, symmetric rewrite: 704 .go files (import statements only; two
disjoint substitutions applied specific-first so the nested gitops-engine
module path never double-matches), plus go.mod (module line, the
gitops-engine require+replace pair, version reset to v0.0.0-... since a
suffix-less path can't carry a v3 semver), gitops-engine/go.mod, Makefile
(ldflags PACKAGE + 2 TEST_MODULE filters), .golangci.yaml, .goreleaser.yaml,
.mockery.yaml (+ gitops-engine's), Procfile, the codegen/proto generation
scripts (hack/generate-proto.sh, update-codegen.sh, update-openapi.sh), and
20 .proto files' go_package options. Also one OpenTelemetry tracer name
string in gitops-engine/pkg/sync/sync_context.go (a cosmestic label, not an
import, but worth keeping consistent).
CGO_ENABLED=0 go build ./... verified green for both the root module and
the nested gitops-engine module; go vet clean on both; go mod tidy run
(network available) to reconcile go.sum, dependency set unchanged.
Left alone, deliberately: doc-only mentions (docs/snyk/*.html scan reports,
gitops-engine/README.md), a comment-only reference in
.github/workflows/release.yaml, and one Renovate bot config line — none are
build- or codegen-load-bearing. Also left alone: comments citing upstream
GitHub issue URLs (github.com/argoproj/argo-cd/issues/NNNNN) and a handful
of test fixtures using "https://github.com/argoproj/argo-cd.git" as a
representative example repo URL — both are accurate historical/test-data
references, not stale self-references.
---------
Co-authored-by: Hanzo Dev <[email protected]>
896 lines
28 KiB
Go
896 lines
28 KiB
Go
package applicationset
|
|
|
|
import (
|
|
"sort"
|
|
"testing"
|
|
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
cr_fake "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
|
|
"github.com/hanzoai/deploy/gitops-engine/pkg/health"
|
|
"github.com/argoproj/pkg/v2/sync"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
k8scache "k8s.io/client-go/tools/cache"
|
|
|
|
"github.com/hanzoai/deploy/common"
|
|
"github.com/hanzoai/deploy/pkg/apiclient/applicationset"
|
|
appsv1 "github.com/hanzoai/deploy/pkg/apis/application/v1alpha1"
|
|
apps "github.com/hanzoai/deploy/pkg/client/clientset/versioned/fake"
|
|
appinformer "github.com/hanzoai/deploy/pkg/client/informers/externalversions"
|
|
"github.com/hanzoai/deploy/server/rbacpolicy"
|
|
"github.com/hanzoai/deploy/test"
|
|
"github.com/hanzoai/deploy/util/argo"
|
|
"github.com/hanzoai/deploy/util/assets"
|
|
"github.com/hanzoai/deploy/util/db"
|
|
"github.com/hanzoai/deploy/util/rbac"
|
|
"github.com/hanzoai/deploy/util/settings"
|
|
)
|
|
|
|
const (
|
|
testNamespace = "default"
|
|
fakeRepoURL = "https://git.com/repo.git"
|
|
)
|
|
|
|
var testEnableEventList []string = argo.DefaultEnableEventList()
|
|
|
|
func fakeRepo() *appsv1.Repository {
|
|
return &appsv1.Repository{
|
|
Repo: fakeRepoURL,
|
|
}
|
|
}
|
|
|
|
func fakeCluster() *appsv1.Cluster {
|
|
return &appsv1.Cluster{
|
|
Server: "https://cluster-api.example.com",
|
|
Name: "fake-cluster",
|
|
Config: appsv1.ClusterConfig{},
|
|
}
|
|
}
|
|
|
|
// newTestAppSetServer returns an ApplicationSetServer with fake data for testing
|
|
func newTestAppSetServer(t *testing.T, objects ...client.Object) *Server {
|
|
t.Helper()
|
|
server, _ := newTestAppSetServerWithK8sClient(t, objects...)
|
|
return server
|
|
}
|
|
|
|
// newTestAppSetServerWithK8sClient returns an ApplicationSetServer and the kubernetes clientset for testing.
|
|
// Use this variant when you need to create kubernetes resources (e.g., Events) after server creation.
|
|
func newTestAppSetServerWithK8sClient(t *testing.T, objects ...client.Object) (*Server, kubernetes.Interface) {
|
|
t.Helper()
|
|
f := func(enf *rbac.Enforcer) {
|
|
_ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
|
|
enf.SetDefaultRole("role:admin")
|
|
}
|
|
scopedNamespaces := ""
|
|
return newTestAppSetServerWithEnforcerConfigure(t, f, scopedNamespaces, objects...)
|
|
}
|
|
|
|
// newTestNamespacedAppSetServer returns an ApplicationSetServer with fake data for testing with namespaced scope
|
|
func newTestNamespacedAppSetServer(t *testing.T, objects ...client.Object) *Server {
|
|
t.Helper()
|
|
f := func(enf *rbac.Enforcer) {
|
|
_ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
|
|
enf.SetDefaultRole("role:admin")
|
|
}
|
|
scopedNamespaces := "argocd"
|
|
server, _ := newTestAppSetServerWithEnforcerConfigure(t, f, scopedNamespaces, objects...)
|
|
return server
|
|
}
|
|
|
|
func newTestAppSetServerWithEnforcerConfigure(t *testing.T, f func(*rbac.Enforcer), namespace string, objects ...client.Object) (*Server, kubernetes.Interface) {
|
|
t.Helper()
|
|
kubeclientset := fake.NewClientset(&corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: testNamespace,
|
|
Name: "argocd-cm",
|
|
Labels: map[string]string{
|
|
"app.kubernetes.io/part-of": "argocd",
|
|
},
|
|
},
|
|
}, &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "argocd-secret",
|
|
Namespace: testNamespace,
|
|
},
|
|
Data: map[string][]byte{
|
|
"admin.password": []byte("test"),
|
|
"server.secretkey": []byte("test"),
|
|
},
|
|
})
|
|
ctx := t.Context()
|
|
db := db.NewDB(testNamespace, settings.NewSettingsManager(ctx, kubeclientset, testNamespace), kubeclientset)
|
|
_, err := db.CreateRepository(ctx, fakeRepo())
|
|
require.NoError(t, err)
|
|
_, err = db.CreateCluster(ctx, fakeCluster())
|
|
require.NoError(t, err)
|
|
|
|
defaultProj := &appsv1.AppProject{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "default"},
|
|
Spec: appsv1.AppProjectSpec{
|
|
SourceRepos: []string{"*"},
|
|
Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
|
|
},
|
|
}
|
|
myProj := &appsv1.AppProject{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "my-proj", Namespace: "default"},
|
|
Spec: appsv1.AppProjectSpec{
|
|
SourceRepos: []string{"*"},
|
|
Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
|
|
},
|
|
}
|
|
|
|
objects = append(objects, defaultProj, myProj)
|
|
|
|
runtimeObjects := make([]runtime.Object, len(objects))
|
|
for i := range objects {
|
|
runtimeObjects[i] = objects[i]
|
|
}
|
|
fakeAppsClientset := apps.NewSimpleClientset(runtimeObjects...)
|
|
factory := appinformer.NewSharedInformerFactoryWithOptions(fakeAppsClientset, 0, appinformer.WithNamespace(namespace), appinformer.WithTweakListOptions(func(_ *metav1.ListOptions) {}))
|
|
fakeProjLister := factory.Argoproj().V1alpha1().AppProjects().Lister().AppProjects(testNamespace)
|
|
|
|
enforcer := rbac.NewEnforcer(kubeclientset, testNamespace, common.ArgoCDRBACConfigMapName, nil)
|
|
f(enforcer)
|
|
enforcer.SetClaimsEnforcerFunc(rbacpolicy.NewRBACPolicyEnforcer(enforcer, fakeProjLister).EnforceClaims)
|
|
|
|
// populate the app informer with the fake objects
|
|
appInformer := factory.Argoproj().V1alpha1().Applications().Informer()
|
|
// TODO(jessesuen): probably should return cancel function so tests can stop background informer
|
|
// ctx, cancel := context.WithCancel(t.Context())
|
|
go appInformer.Run(ctx.Done())
|
|
if !k8scache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced) {
|
|
panic("Timed out waiting for caches to sync")
|
|
}
|
|
// populate the appset informer with the fake objects
|
|
appsetInformer := factory.Argoproj().V1alpha1().ApplicationSets().Informer()
|
|
go appsetInformer.Run(ctx.Done())
|
|
if !k8scache.WaitForCacheSync(ctx.Done(), appsetInformer.HasSynced) {
|
|
t.Fatal("Timed out waiting for caches to sync")
|
|
}
|
|
|
|
scheme := runtime.NewScheme()
|
|
err = appsv1.AddToScheme(scheme)
|
|
require.NoError(t, err)
|
|
err = corev1.AddToScheme(scheme)
|
|
require.NoError(t, err)
|
|
|
|
// Add the fake cluster secret so the ClusterGenerator can find it via controller-runtime client
|
|
fakeClusterSecret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "cluster-cluster-api.example.com-2aborni",
|
|
Namespace: testNamespace,
|
|
Labels: map[string]string{
|
|
common.LabelKeySecretType: common.LabelValueSecretTypeCluster,
|
|
},
|
|
},
|
|
Data: map[string][]byte{
|
|
"name": []byte("fake-cluster"),
|
|
"server": []byte("https://cluster-api.example.com"),
|
|
"config": []byte("{}"),
|
|
},
|
|
}
|
|
objects = append(objects, fakeClusterSecret)
|
|
|
|
crClient := cr_fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
|
|
|
|
projInformer := factory.Argoproj().V1alpha1().AppProjects().Informer()
|
|
go projInformer.Run(ctx.Done())
|
|
if !k8scache.WaitForCacheSync(ctx.Done(), projInformer.HasSynced) {
|
|
panic("Timed out waiting for caches to sync")
|
|
}
|
|
|
|
clusterInformer, err := settings.NewClusterInformer(kubeclientset, testNamespace)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer test.StartInformer(clusterInformer)()
|
|
|
|
server := NewServer(
|
|
db,
|
|
kubeclientset,
|
|
nil,
|
|
crClient,
|
|
enforcer,
|
|
nil,
|
|
fakeAppsClientset,
|
|
appsetInformer,
|
|
factory.Argoproj().V1alpha1().ApplicationSets().Lister(),
|
|
nil,
|
|
testNamespace,
|
|
sync.NewKeyLock(),
|
|
[]string{testNamespace, "external-namespace"},
|
|
true,
|
|
true,
|
|
"",
|
|
[]string{},
|
|
true,
|
|
true,
|
|
testEnableEventList,
|
|
clusterInformer,
|
|
)
|
|
return server.(*Server), kubeclientset
|
|
}
|
|
|
|
func newTestAppSet(opts ...func(appset *appsv1.ApplicationSet)) *appsv1.ApplicationSet {
|
|
appset := appsv1.ApplicationSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: testNamespace,
|
|
},
|
|
Spec: appsv1.ApplicationSetSpec{
|
|
Template: appsv1.ApplicationSetTemplate{
|
|
Spec: appsv1.ApplicationSpec{
|
|
Project: "default",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for i := range opts {
|
|
opts[i](&appset)
|
|
}
|
|
return &appset
|
|
}
|
|
|
|
func testListAppsetsWithLabels(t *testing.T, appsetQuery applicationset.ApplicationSetListQuery, appServer *Server) {
|
|
t.Helper()
|
|
validTests := []struct {
|
|
testName string
|
|
label string
|
|
expectedResult []string
|
|
}{
|
|
{
|
|
testName: "Equality based filtering using '=' operator",
|
|
label: "key1=value1",
|
|
expectedResult: []string{"AppSet1"},
|
|
},
|
|
{
|
|
testName: "Equality based filtering using '==' operator",
|
|
label: "key1==value1",
|
|
expectedResult: []string{"AppSet1"},
|
|
},
|
|
{
|
|
testName: "Equality based filtering using '!=' operator",
|
|
label: "key1!=value1",
|
|
expectedResult: []string{"AppSet2", "AppSet3"},
|
|
},
|
|
{
|
|
testName: "Set based filtering using 'in' operator",
|
|
label: "key1 in (value1, value3)",
|
|
expectedResult: []string{"AppSet1", "AppSet3"},
|
|
},
|
|
{
|
|
testName: "Set based filtering using 'notin' operator",
|
|
label: "key1 notin (value1, value3)",
|
|
expectedResult: []string{"AppSet2"},
|
|
},
|
|
{
|
|
testName: "Set based filtering using 'exists' operator",
|
|
label: "key1",
|
|
expectedResult: []string{"AppSet1", "AppSet2", "AppSet3"},
|
|
},
|
|
{
|
|
testName: "Set based filtering using 'not exists' operator",
|
|
label: "!key2",
|
|
expectedResult: []string{"AppSet2", "AppSet3"},
|
|
},
|
|
}
|
|
// test valid scenarios
|
|
for _, validTest := range validTests {
|
|
t.Run(validTest.testName, func(t *testing.T) {
|
|
appsetQuery.Selector = validTest.label
|
|
res, err := appServer.List(t.Context(), &appsetQuery)
|
|
require.NoError(t, err)
|
|
apps := []string{}
|
|
for i := range res.Items {
|
|
apps = append(apps, res.Items[i].Name)
|
|
}
|
|
assert.Equal(t, validTest.expectedResult, apps)
|
|
})
|
|
}
|
|
|
|
invalidTests := []struct {
|
|
testName string
|
|
label string
|
|
errorMesage string
|
|
}{
|
|
{
|
|
testName: "Set based filtering using '>' operator",
|
|
label: "key1>value1",
|
|
errorMesage: "error parsing the selector",
|
|
},
|
|
{
|
|
testName: "Set based filtering using '<' operator",
|
|
label: "key1<value1",
|
|
errorMesage: "error parsing the selector",
|
|
},
|
|
}
|
|
// test invalid scenarios
|
|
for _, invalidTest := range invalidTests {
|
|
t.Run(invalidTest.testName, func(t *testing.T) {
|
|
appsetQuery.Selector = invalidTest.label
|
|
_, err := appServer.List(t.Context(), &appsetQuery)
|
|
assert.ErrorContains(t, err, invalidTest.errorMesage)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListAppSetsInNamespaceWithLabels(t *testing.T) {
|
|
testNamespace := "test-namespace"
|
|
appSetServer := newTestAppSetServer(t, newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet1"
|
|
appset.Namespace = testNamespace
|
|
appset.SetLabels(map[string]string{"key1": "value1", "key2": "value1"})
|
|
}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet2"
|
|
appset.Namespace = testNamespace
|
|
appset.SetLabels(map[string]string{"key1": "value2"})
|
|
}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet3"
|
|
appset.Namespace = testNamespace
|
|
appset.SetLabels(map[string]string{"key1": "value3"})
|
|
}))
|
|
appSetServer.enabledNamespaces = []string{testNamespace}
|
|
appsetQuery := applicationset.ApplicationSetListQuery{AppsetNamespace: testNamespace}
|
|
|
|
testListAppsetsWithLabels(t, appsetQuery, appSetServer)
|
|
}
|
|
|
|
func TestListAppSetsInDefaultNSWithLabels(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet1"
|
|
appset.SetLabels(map[string]string{"key1": "value1", "key2": "value1"})
|
|
}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet2"
|
|
appset.SetLabels(map[string]string{"key1": "value2"})
|
|
}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet3"
|
|
appset.SetLabels(map[string]string{"key1": "value3"})
|
|
}))
|
|
appsetQuery := applicationset.ApplicationSetListQuery{}
|
|
|
|
testListAppsetsWithLabels(t, appsetQuery, appSetServer)
|
|
}
|
|
|
|
// This test covers https://github.com/argoproj/argo-cd/issues/15429
|
|
// If the namespace isn't provided during listing action, argocd's
|
|
// default namespace must be used and not all the namespaces
|
|
func TestListAppSetsWithoutNamespace(t *testing.T) {
|
|
testNamespace := "test-namespace"
|
|
appSetServer := newTestNamespacedAppSetServer(t, newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet1"
|
|
appset.Namespace = testNamespace
|
|
appset.SetLabels(map[string]string{"key1": "value1", "key2": "value1"})
|
|
}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet2"
|
|
appset.Namespace = testNamespace
|
|
appset.SetLabels(map[string]string{"key1": "value2"})
|
|
}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet3"
|
|
appset.Namespace = testNamespace
|
|
appset.SetLabels(map[string]string{"key1": "value3"})
|
|
}))
|
|
appSetServer.enabledNamespaces = []string{testNamespace}
|
|
appsetQuery := applicationset.ApplicationSetListQuery{}
|
|
|
|
res, err := appSetServer.List(t.Context(), &appsetQuery)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, res.Items)
|
|
}
|
|
|
|
func TestCreateAppSet(t *testing.T) {
|
|
testAppSet := newTestAppSet()
|
|
appServer := newTestAppSetServer(t)
|
|
testAppSet.Spec.Generators = []appsv1.ApplicationSetGenerator{
|
|
{
|
|
List: &appsv1.ListGenerator{},
|
|
},
|
|
}
|
|
createReq := applicationset.ApplicationSetCreateRequest{
|
|
Applicationset: testAppSet,
|
|
}
|
|
_, err := appServer.Create(t.Context(), &createReq)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCreateAppSetTemplatedProject(t *testing.T) {
|
|
testAppSet := newTestAppSet()
|
|
appServer := newTestAppSetServer(t)
|
|
testAppSet.Spec.Template.Spec.Project = "{{ .project }}"
|
|
createReq := applicationset.ApplicationSetCreateRequest{
|
|
Applicationset: testAppSet,
|
|
}
|
|
_, err := appServer.Create(t.Context(), &createReq)
|
|
assert.EqualError(t, err, "error validating ApplicationSets: the Argo CD API does not currently support creating ApplicationSets with templated `project` fields")
|
|
}
|
|
|
|
func TestCreateAppSetWrongNamespace(t *testing.T) {
|
|
testAppSet := newTestAppSet()
|
|
appServer := newTestAppSetServer(t)
|
|
testAppSet.Namespace = "NOT-ALLOWED"
|
|
createReq := applicationset.ApplicationSetCreateRequest{
|
|
Applicationset: testAppSet,
|
|
}
|
|
_, err := appServer.Create(t.Context(), &createReq)
|
|
|
|
assert.EqualError(t, err, "namespace 'NOT-ALLOWED' is not permitted")
|
|
}
|
|
|
|
func TestCreateAppSetDryRun(t *testing.T) {
|
|
testAppSet := newTestAppSet()
|
|
appServer := newTestAppSetServer(t)
|
|
testAppSet.Spec.Template.Name = "{{name}}"
|
|
testAppSet.Spec.Generators = []appsv1.ApplicationSetGenerator{
|
|
{
|
|
List: &appsv1.ListGenerator{
|
|
Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"name": "a"}`)}, {Raw: []byte(`{"name": "b"}`)}},
|
|
},
|
|
},
|
|
}
|
|
createReq := applicationset.ApplicationSetCreateRequest{
|
|
Applicationset: testAppSet,
|
|
DryRun: true,
|
|
}
|
|
result, err := appServer.Create(t.Context(), &createReq)
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, result.Status.Resources, 2)
|
|
|
|
// Sort resulting application by name
|
|
sort.Slice(result.Status.Resources, func(i, j int) bool {
|
|
return result.Status.Resources[i].Name < result.Status.Resources[j].Name
|
|
})
|
|
|
|
assert.Equal(t, "a", result.Status.Resources[0].Name)
|
|
assert.Equal(t, testAppSet.Namespace, result.Status.Resources[0].Namespace)
|
|
assert.Equal(t, "b", result.Status.Resources[1].Name)
|
|
assert.Equal(t, testAppSet.Namespace, result.Status.Resources[1].Namespace)
|
|
}
|
|
|
|
func TestCreateAppSetDryRunWithDuplicate(t *testing.T) {
|
|
testAppSet := newTestAppSet()
|
|
appServer := newTestAppSetServer(t)
|
|
testAppSet.Spec.Template.Name = "{{name}}"
|
|
testAppSet.Spec.Generators = []appsv1.ApplicationSetGenerator{
|
|
{
|
|
List: &appsv1.ListGenerator{
|
|
Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"name": "a"}`)}, {Raw: []byte(`{"name": "a"}`)}},
|
|
},
|
|
},
|
|
}
|
|
createReq := applicationset.ApplicationSetCreateRequest{
|
|
Applicationset: testAppSet,
|
|
DryRun: true,
|
|
}
|
|
result, err := appServer.Create(t.Context(), &createReq)
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, result.Status.Resources, 1)
|
|
assert.Equal(t, "a", result.Status.Resources[0].Name)
|
|
assert.Equal(t, testAppSet.Namespace, result.Status.Resources[0].Namespace)
|
|
}
|
|
|
|
func TestGetAppSet(t *testing.T) {
|
|
appSet1 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet1"
|
|
})
|
|
|
|
appSet2 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet2"
|
|
})
|
|
|
|
appSet3 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet3"
|
|
})
|
|
|
|
t.Run("Get in default namespace", func(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
|
|
|
|
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1"}
|
|
|
|
res, err := appSetServer.Get(t.Context(), &appsetQuery)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "AppSet1", res.Name)
|
|
})
|
|
|
|
t.Run("Get in named namespace", func(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
|
|
|
|
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1", AppsetNamespace: testNamespace}
|
|
|
|
res, err := appSetServer.Get(t.Context(), &appsetQuery)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "AppSet1", res.Name)
|
|
})
|
|
|
|
t.Run("Get in not allowed namespace", func(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
|
|
|
|
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1", AppsetNamespace: "NOT-ALLOWED"}
|
|
|
|
_, err := appSetServer.Get(t.Context(), &appsetQuery)
|
|
assert.EqualError(t, err, "namespace 'NOT-ALLOWED' is not permitted")
|
|
})
|
|
}
|
|
|
|
func TestDeleteAppSet(t *testing.T) {
|
|
appSet1 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet1"
|
|
})
|
|
|
|
appSet2 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet2"
|
|
})
|
|
|
|
appSet3 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet3"
|
|
})
|
|
|
|
t.Run("Delete in default namespace", func(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
|
|
|
|
appsetQuery := applicationset.ApplicationSetDeleteRequest{Name: "AppSet1"}
|
|
|
|
res, err := appSetServer.Delete(t.Context(), &appsetQuery)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, &applicationset.ApplicationSetResponse{}, res)
|
|
})
|
|
|
|
t.Run("Delete in named namespace", func(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
|
|
|
|
appsetQuery := applicationset.ApplicationSetDeleteRequest{Name: "AppSet1", AppsetNamespace: testNamespace}
|
|
|
|
res, err := appSetServer.Delete(t.Context(), &appsetQuery)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, &applicationset.ApplicationSetResponse{}, res)
|
|
})
|
|
}
|
|
|
|
func TestUpdateAppSet(t *testing.T) {
|
|
appSet := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Annotations = map[string]string{
|
|
"annotation-key1": "annotation-value1",
|
|
"annotation-key2": "annotation-value2",
|
|
}
|
|
appset.Labels = map[string]string{
|
|
"label-key1": "label-value1",
|
|
"label-key2": "label-value2",
|
|
}
|
|
appset.Finalizers = []string{"finalizer"}
|
|
})
|
|
|
|
newAppSet := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Annotations = map[string]string{
|
|
"annotation-key1": "annotation-value1-updated",
|
|
}
|
|
appset.Labels = map[string]string{
|
|
"label-key1": "label-value1-updated",
|
|
}
|
|
appset.Finalizers = []string{"finalizer-updated"}
|
|
})
|
|
|
|
t.Run("Update merge", func(t *testing.T) {
|
|
appServer := newTestAppSetServer(t, appSet)
|
|
|
|
updated, err := appServer.updateAppSet(t.Context(), appSet, newAppSet, true)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, map[string]string{
|
|
"annotation-key1": "annotation-value1-updated",
|
|
"annotation-key2": "annotation-value2",
|
|
}, updated.Annotations)
|
|
assert.Equal(t, map[string]string{
|
|
"label-key1": "label-value1-updated",
|
|
"label-key2": "label-value2",
|
|
}, updated.Labels)
|
|
assert.Equal(t, []string{"finalizer-updated"}, updated.Finalizers)
|
|
})
|
|
|
|
t.Run("Update no merge", func(t *testing.T) {
|
|
appServer := newTestAppSetServer(t, appSet)
|
|
|
|
updated, err := appServer.updateAppSet(t.Context(), appSet, newAppSet, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, map[string]string{
|
|
"annotation-key1": "annotation-value1-updated",
|
|
}, updated.Annotations)
|
|
assert.Equal(t, map[string]string{
|
|
"label-key1": "label-value1-updated",
|
|
}, updated.Labels)
|
|
})
|
|
}
|
|
|
|
func TestUpsertAppSet(t *testing.T) {
|
|
name := "test"
|
|
ns := "external-namespace"
|
|
appSet := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Annotations = map[string]string{
|
|
"annotation-key1": "annotation-value1",
|
|
"annotation-key2": "annotation-value2",
|
|
}
|
|
appset.Labels = map[string]string{
|
|
"label-key1": "label-value1",
|
|
"label-key2": "label-value2",
|
|
}
|
|
appset.Name = name
|
|
appset.Namespace = ns
|
|
appset.Finalizers = []string{"finalizer"}
|
|
})
|
|
|
|
updatedAppSet := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Annotations = map[string]string{
|
|
"annotation-key1": "annotation-value1-updated",
|
|
}
|
|
appset.Labels = map[string]string{
|
|
"label-key1": "label-value1-updated",
|
|
}
|
|
appset.Name = name
|
|
appset.Namespace = ns
|
|
appset.Finalizers = []string{"finalizer-updated"}
|
|
})
|
|
|
|
t.Run("Upsert", func(t *testing.T) {
|
|
appServer := newTestAppSetServer(t, appSet)
|
|
|
|
updated, err := appServer.Create(t.Context(), &applicationset.ApplicationSetCreateRequest{
|
|
Applicationset: updatedAppSet,
|
|
Upsert: true,
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, map[string]string{
|
|
"annotation-key1": "annotation-value1-updated",
|
|
"annotation-key2": "annotation-value2",
|
|
}, updated.Annotations)
|
|
assert.Equal(t, map[string]string{
|
|
"label-key1": "label-value1-updated",
|
|
"label-key2": "label-value2",
|
|
}, updated.Labels)
|
|
assert.Equal(t, []string{"finalizer-updated"}, updated.Finalizers)
|
|
})
|
|
}
|
|
|
|
func TestResourceTree(t *testing.T) {
|
|
appSet1 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet1"
|
|
appset.Status.Resources = []appsv1.ResourceStatus{
|
|
{
|
|
Name: "app1",
|
|
Kind: "Application",
|
|
Group: "argoproj.io",
|
|
Version: "v1alpha1",
|
|
Namespace: "default",
|
|
Health: &appsv1.HealthStatus{
|
|
Status: health.HealthStatusHealthy,
|
|
Message: "OK",
|
|
},
|
|
Status: appsv1.SyncStatusCodeSynced,
|
|
},
|
|
}
|
|
})
|
|
|
|
appSet2 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet2"
|
|
})
|
|
|
|
appSet3 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet3"
|
|
})
|
|
|
|
expectedTree := &appsv1.ApplicationSetTree{
|
|
Nodes: []appsv1.ResourceNode{
|
|
{
|
|
ResourceRef: appsv1.ResourceRef{
|
|
Kind: "Application",
|
|
Group: "argoproj.io",
|
|
Version: "v1alpha1",
|
|
Namespace: "default",
|
|
Name: "app1",
|
|
},
|
|
ParentRefs: []appsv1.ResourceRef{
|
|
{
|
|
Kind: "ApplicationSet",
|
|
Group: "argoproj.io",
|
|
Version: "v1alpha1",
|
|
Namespace: "default",
|
|
Name: "AppSet1",
|
|
},
|
|
},
|
|
Health: &appsv1.HealthStatus{
|
|
Status: health.HealthStatusHealthy,
|
|
Message: "OK",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
t.Run("ResourceTree in default namespace", func(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
|
|
|
|
appsetQuery := applicationset.ApplicationSetTreeQuery{Name: "AppSet1"}
|
|
|
|
res, err := appSetServer.ResourceTree(t.Context(), &appsetQuery)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedTree, res)
|
|
})
|
|
|
|
t.Run("ResourceTree in named namespace", func(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
|
|
|
|
appsetQuery := applicationset.ApplicationSetTreeQuery{Name: "AppSet1", AppsetNamespace: testNamespace}
|
|
|
|
res, err := appSetServer.ResourceTree(t.Context(), &appsetQuery)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedTree, res)
|
|
})
|
|
|
|
t.Run("ResourceTree in not allowed namespace", func(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
|
|
|
|
appsetQuery := applicationset.ApplicationSetTreeQuery{Name: "AppSet1", AppsetNamespace: "NOT-ALLOWED"}
|
|
|
|
_, err := appSetServer.ResourceTree(t.Context(), &appsetQuery)
|
|
assert.EqualError(t, err, "namespace 'NOT-ALLOWED' is not permitted")
|
|
})
|
|
}
|
|
|
|
func TestListResourceEvents(t *testing.T) {
|
|
appSet1 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet1"
|
|
appset.UID = "test-uid-123"
|
|
})
|
|
|
|
appSet2 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet2"
|
|
})
|
|
|
|
t.Run("ListResourceEvents in default namespace", func(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, appSet1, appSet2)
|
|
|
|
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1"}
|
|
|
|
res, err := appSetServer.ListResourceEvents(t.Context(), &appsetQuery)
|
|
require.NoError(t, err)
|
|
// No events exist in the fake client, so we expect an empty list
|
|
assert.Empty(t, res.Items)
|
|
})
|
|
|
|
t.Run("ListResourceEvents in named namespace", func(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, appSet1, appSet2)
|
|
|
|
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1", AppsetNamespace: testNamespace}
|
|
|
|
res, err := appSetServer.ListResourceEvents(t.Context(), &appsetQuery)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, res.Items)
|
|
})
|
|
|
|
t.Run("ListResourceEvents in not allowed namespace", func(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, appSet1, appSet2)
|
|
|
|
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1", AppsetNamespace: "NOT-ALLOWED"}
|
|
|
|
_, err := appSetServer.ListResourceEvents(t.Context(), &appsetQuery)
|
|
assert.EqualError(t, err, "namespace 'NOT-ALLOWED' is not permitted")
|
|
})
|
|
|
|
t.Run("ListResourceEvents for non-existent appset", func(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, appSet1, appSet2)
|
|
|
|
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "DoesNotExist"}
|
|
|
|
_, err := appSetServer.ListResourceEvents(t.Context(), &appsetQuery)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "error getting ApplicationSet")
|
|
})
|
|
|
|
t.Run("ListResourceEvents with events returns non-empty list", func(t *testing.T) {
|
|
appSetServer, kubeclientset := newTestAppSetServerWithK8sClient(t, appSet1, appSet2)
|
|
|
|
// Create events after server creation using the kubeclientset
|
|
_, err := kubeclientset.CoreV1().Events(testNamespace).Create(t.Context(), &corev1.Event{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "appset1-event-1",
|
|
Namespace: testNamespace,
|
|
},
|
|
InvolvedObject: corev1.ObjectReference{
|
|
Name: "AppSet1",
|
|
Namespace: testNamespace,
|
|
UID: "test-uid-123",
|
|
},
|
|
Reason: "Created",
|
|
Message: "ApplicationSet created successfully",
|
|
Type: corev1.EventTypeNormal,
|
|
}, metav1.CreateOptions{})
|
|
require.NoError(t, err)
|
|
|
|
_, err = kubeclientset.CoreV1().Events(testNamespace).Create(t.Context(), &corev1.Event{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "appset1-event-2",
|
|
Namespace: testNamespace,
|
|
},
|
|
InvolvedObject: corev1.ObjectReference{
|
|
Name: "AppSet1",
|
|
Namespace: testNamespace,
|
|
UID: "test-uid-123",
|
|
},
|
|
Reason: "Updated",
|
|
Message: "ApplicationSet updated successfully",
|
|
Type: corev1.EventTypeNormal,
|
|
}, metav1.CreateOptions{})
|
|
require.NoError(t, err)
|
|
|
|
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1"}
|
|
|
|
res, err := appSetServer.ListResourceEvents(t.Context(), &appsetQuery)
|
|
require.NoError(t, err)
|
|
assert.Len(t, res.Items, 2)
|
|
|
|
eventNames := make([]string, 0, len(res.Items))
|
|
for _, item := range res.Items {
|
|
eventNames = append(eventNames, item.Metadata.Name)
|
|
}
|
|
assert.Contains(t, eventNames, "appset1-event-1")
|
|
assert.Contains(t, eventNames, "appset1-event-2")
|
|
})
|
|
}
|
|
|
|
func TestAppSet_Generate_Cluster(t *testing.T) {
|
|
appSet1 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
|
|
appset.Name = "AppSet1"
|
|
appset.Spec.Template.Name = "{{name}}"
|
|
appset.Spec.Generators = []appsv1.ApplicationSetGenerator{
|
|
{
|
|
Clusters: &appsv1.ClusterGenerator{},
|
|
},
|
|
}
|
|
})
|
|
|
|
t.Run("Generate in default namespace", func(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, appSet1)
|
|
appsetQuery := applicationset.ApplicationSetGenerateRequest{
|
|
ApplicationSet: appSet1,
|
|
}
|
|
|
|
res, err := appSetServer.Generate(t.Context(), &appsetQuery)
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Applications, 2)
|
|
assert.Equal(t, "fake-cluster", res.Applications[0].Name)
|
|
assert.Equal(t, "in-cluster", res.Applications[1].Name)
|
|
})
|
|
|
|
t.Run("Generate in different namespace", func(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, appSet1)
|
|
|
|
appSet1Ns := appSet1.DeepCopy()
|
|
appSet1Ns.Namespace = "external-namespace"
|
|
appsetQuery := applicationset.ApplicationSetGenerateRequest{ApplicationSet: appSet1Ns}
|
|
|
|
res, err := appSetServer.Generate(t.Context(), &appsetQuery)
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Applications, 2)
|
|
assert.Equal(t, "fake-cluster", res.Applications[0].Name)
|
|
assert.Equal(t, "in-cluster", res.Applications[1].Name)
|
|
})
|
|
|
|
t.Run("Generate in not allowed namespace", func(t *testing.T) {
|
|
appSetServer := newTestAppSetServer(t, appSet1)
|
|
|
|
appSet1Ns := appSet1.DeepCopy()
|
|
appSet1Ns.Namespace = "NOT-ALLOWED"
|
|
|
|
appsetQuery := applicationset.ApplicationSetGenerateRequest{ApplicationSet: appSet1Ns}
|
|
|
|
_, err := appSetServer.Generate(t.Context(), &appsetQuery)
|
|
assert.EqualError(t, err, "namespace 'NOT-ALLOWED' is not permitted")
|
|
})
|
|
}
|