internal/argocd/dashboard_test.go
Rama main · réplica de lectura del repositorio original.
package argocd
import (
"context"
"errors"
"maps"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/sophotechlabs/spinoza/internal/api"
)
type stubLister struct {
items map[string][]*unstructured.Unstructured
errs map[string]error
warmed []api.ResourceDescriptor
}
func (s *stubLister) List(
_ context.Context,
desc api.ResourceDescriptor,
) ([]*unstructured.Unstructured, error) {
err, refused := s.errs[desc.Resource]
if refused {
return nil, err
}
return s.items[desc.Resource], nil
}
func (s *stubLister) Warm(_ context.Context, descs []api.ResourceDescriptor) {
s.warmed = append(s.warmed, descs...)
}
func appDescriptor() api.ResourceDescriptor {
return api.ResourceDescriptor{
Group: Group,
Version: "v1alpha1",
Resource: applications,
Kind: "Application",
Namespaced: true,
}
}
func setDescriptor() api.ResourceDescriptor {
return api.ResourceDescriptor{
Group: Group,
Version: "v1alpha1",
Resource: applicationSets,
Kind: "ApplicationSet",
Namespaced: true,
}
}
func projectDescriptor() api.ResourceDescriptor {
return api.ResourceDescriptor{
Group: Group,
Version: "v1alpha1",
Resource: appProjects,
Kind: "AppProject",
Namespaced: true,
}
}
func catalog(descs ...api.ResourceDescriptor) map[string]api.ResourceDescriptor {
out := map[string]api.ResourceDescriptor{}
for _, desc := range descs {
out[desc.Group+"/"+desc.Version+"/"+desc.Resource] = desc
}
out["apps/v1/deployments"] = api.ResourceDescriptor{
Group: "apps", Version: "v1", Resource: "deployments", Kind: "Deployment",
}
return out
}
func app(name string, fields map[string]any) *unstructured.Unstructured {
object := map[string]any{
"apiVersion": "argoproj.io/v1alpha1",
"kind": "Application",
"metadata": map[string]any{"name": name, "namespace": "argocd"},
}
maps.Copy(object, fields)
return &unstructured.Unstructured{Object: object}
}
func healthy(name, revision string) *unstructured.Unstructured {
return app(name, map[string]any{
"spec": map[string]any{
"project": "default",
"source": map[string]any{"repoURL": "https://git/apps", "path": "apps/" + name},
"destination": map[string]any{"server": "in-cluster", "namespace": name},
},
"status": map[string]any{
"sync": map[string]any{"status": "Synced", "revision": revision},
"health": map[string]any{"status": "Healthy"},
},
})
}
func onlyApps(items ...*unstructured.Unstructured) *stubLister {
return &stubLister{items: map[string][]*unstructured.Unstructured{applications: items}}
}
func build(t *testing.T, lister Lister, descs map[string]api.ResourceDescriptor) api.ArgoDashboard {
t.Helper()
return Build(context.Background(), lister, descs)
}
func TestABuildWithoutArgoTypesAsksForNothing(t *testing.T) {
lister := onlyApps(healthy("web", "abc123"))
found := build(t, lister, catalog())
if len(found.Apps) != 0 {
t.Fatalf("apps = %+v, want none", found.Apps)
}
if len(lister.warmed) != 0 {
t.Fatalf("warmed %d types on a cluster with no argo", len(lister.warmed))
}
}
func TestArgoTypesOutsideTheDashboardAreNotListed(t *testing.T) {
descs := catalog(api.ResourceDescriptor{
Group: Group, Version: "v1alpha1", Resource: "appprojecttokens", Kind: "AppProjectToken",
})
if got := wanted(descs); len(got) != 0 {
t.Fatalf("wanted = %+v, want no unsupported Argo types", got)
}
}
func TestApplicationsCarryTheirSyncAndHealth(t *testing.T) {
lister := onlyApps(healthy("web", "abc123"))
found := build(t, lister, catalog(appDescriptor()))
if len(found.Apps) != 1 {
t.Fatalf("apps = %+v, want one", found.Apps)
}
got := found.Apps[0]
if got.Sync != "Synced" || got.Health != "Healthy" {
t.Fatalf("status = %s/%s", got.Sync, got.Health)
}
if got.Project != "default" || got.Repo != "https://git/apps" || got.Path != "apps/web" {
t.Fatalf("app = %+v", got)
}
if got.Destination != "in-cluster web" {
t.Fatalf("destination = %q", got.Destination)
}
if got.Revision != "abc123" {
t.Fatalf("revision = %q", got.Revision)
}
if got.Kind != "Application" {
t.Fatalf("kind = %q", got.Kind)
}
}
func TestApplicationsComeBackInAStableOrder(t *testing.T) {
first := healthy("web", "a")
first.SetNamespace("team-b")
second := healthy("api", "b")
second.SetNamespace("team-a")
lister := onlyApps(first, second)
found := build(t, lister, catalog(appDescriptor()))
if found.Apps[0].Name != "api" || found.Apps[1].Name != "web" {
t.Fatalf("order = %s, %s", found.Apps[0].Name, found.Apps[1].Name)
}
}
func TestAnAppOfAppsChildNamesItsParent(t *testing.T) {
parent := healthy("root", "a")
child := healthy("web", "b")
child.SetLabels(map[string]string{trackingLabel: "root"})
lister := onlyApps(parent, child)
found := build(t, lister, catalog(appDescriptor()))
for _, got := range found.Apps {
if got.Name == "web" && got.Owner != "root" {
t.Fatalf("owner = %q, want root", got.Owner)
}
if got.Name == "root" && got.Owner != "" {
t.Fatalf("the parent claims an owner: %q", got.Owner)
}
}
}
func TestTheTrackingAnnotationAlsoNamesTheParent(t *testing.T) {
parent := healthy("root", "a")
child := healthy("web", "b")
child.SetAnnotations(map[string]string{
trackingAnnotation: "root:argoproj.io/Application:argocd/web",
})
lister := onlyApps(parent, child)
found := build(t, lister, catalog(appDescriptor()))
for _, got := range found.Apps {
if got.Name == "web" && got.Owner != "root" {
t.Fatalf("owner = %q, want root", got.Owner)
}
}
}
func TestAnAppTrackedByItselfHasNoParent(t *testing.T) {
only := healthy("web", "a")
only.SetLabels(map[string]string{trackingLabel: "web"})
lister := onlyApps(only)
found := build(t, lister, catalog(appDescriptor()))
if found.Apps[0].Owner != "" {
t.Fatalf("owner = %q, want none", found.Apps[0].Owner)
}
}
func TestAParentThatIsNotAnApplicationIsDropped(t *testing.T) {
child := healthy("web", "a")
child.SetLabels(map[string]string{trackingLabel: "some-helm-release"})
lister := onlyApps(child)
found := build(t, lister, catalog(appDescriptor()))
if found.Apps[0].Owner != "" {
t.Fatalf("owner = %q, want none for a parent that is not an app", found.Apps[0].Owner)
}
}
func TestAGeneratedAppNamesItsApplicationSet(t *testing.T) {
child := healthy("web", "a")
child.SetOwnerReferences([]metav1.OwnerReference{{Kind: "ApplicationSet", Name: "fleet"}})
lister := &stubLister{
items: map[string][]*unstructured.Unstructured{
applications: {child},
applicationSets: {app("fleet", map[string]any{})},
},
}
found := build(t, lister, catalog(appDescriptor(), setDescriptor()))
if found.Apps[0].Owner != "fleet" {
t.Fatalf("owner = %q, want the application set", found.Apps[0].Owner)
}
if len(found.ApplicationSets) != 1 {
t.Fatalf("sets = %+v, want one", found.ApplicationSets)
}
}
func TestAnAppWithSeveralSourcesStillNamesOne(t *testing.T) {
many := app("web", map[string]any{
"spec": map[string]any{
"sources": []any{
map[string]any{"repoURL": "https://git/values", "targetRevision": "main"},
map[string]any{"repoURL": "https://git/chart", "chart": "podinfo"},
},
},
})
lister := onlyApps(many)
found := build(t, lister, catalog(appDescriptor()))
if found.Apps[0].Repo != "https://git/values" {
t.Fatalf("repo = %q", found.Apps[0].Repo)
}
if found.Apps[0].Path != "podinfo" {
t.Fatalf("path = %q, want the chart when there is no path", found.Apps[0].Path)
}
if found.Apps[0].Revision != "main" {
t.Fatalf("revision = %q, want the target when nothing synced yet", found.Apps[0].Revision)
}
}
func TestAnAppWithNoStatusIsStillListed(t *testing.T) {
bare := app("web", map[string]any{"spec": map[string]any{"project": "default"}})
lister := onlyApps(bare)
found := build(t, lister, catalog(appDescriptor()))
if len(found.Apps) != 1 {
t.Fatalf("apps = %+v, want the app", found.Apps)
}
if found.Apps[0].Sync != "" || found.Apps[0].Health != "" {
t.Fatalf("app = %+v, want empty status", found.Apps[0])
}
}
func TestAHealthMessageIsCarried(t *testing.T) {
sick := app("web", map[string]any{
"status": map[string]any{
"health": map[string]any{"status": "Degraded", "message": "pod crashlooping"},
},
})
lister := onlyApps(sick)
found := build(t, lister, catalog(appDescriptor()))
if found.Apps[0].Message != "pod crashlooping" {
t.Fatalf("message = %q", found.Apps[0].Message)
}
}
func TestAnOperationMessageStandsInForHealth(t *testing.T) {
failing := app("web", map[string]any{
"status": map[string]any{
"operationState": map[string]any{"message": "one or more objects failed"},
},
})
lister := onlyApps(failing)
found := build(t, lister, catalog(appDescriptor()))
if found.Apps[0].Message != "one or more objects failed" {
t.Fatalf("message = %q", found.Apps[0].Message)
}
}
func TestADestinationWithoutANamespaceIsJustTheServer(t *testing.T) {
remote := app("web", map[string]any{
"spec": map[string]any{"destination": map[string]any{"server": "https://kube"}},
})
lister := onlyApps(remote)
found := build(t, lister, catalog(appDescriptor()))
if found.Apps[0].Destination != "https://kube" {
t.Fatalf("destination = %q", found.Apps[0].Destination)
}
}
func TestADestinationWithoutAServerIsJustTheNamespace(t *testing.T) {
local := app("web", map[string]any{
"spec": map[string]any{"destination": map[string]any{"namespace": "shop"}},
})
lister := onlyApps(local)
found := build(t, lister, catalog(appDescriptor()))
if found.Apps[0].Destination != "shop" {
t.Fatalf("destination = %q", found.Apps[0].Destination)
}
}
func TestAKindThatCannotBeListedIsReported(t *testing.T) {
lister := &stubLister{errs: map[string]error{applications: errors.New("applications is forbidden")}}
found := build(t, lister, catalog(appDescriptor()))
if found.Error == "" {
t.Fatal("a refusal was swallowed")
}
if len(found.Apps) != 0 {
t.Fatalf("apps = %+v, want none", found.Apps)
}
}
func TestApplicationsStillArriveWhenTheSetsAreRefused(t *testing.T) {
lister := &stubLister{
items: map[string][]*unstructured.Unstructured{applications: {healthy("web", "a")}},
errs: map[string]error{applicationSets: errors.New("applicationsets is forbidden")},
}
found := build(t, lister, catalog(appDescriptor(), setDescriptor()))
if len(found.Apps) != 1 {
t.Fatalf("apps = %+v, want the application", found.Apps)
}
if found.Error == "" {
t.Fatal("the refusal was swallowed")
}
}
func TestProjectsComeBackOnTheirOwn(t *testing.T) {
lister := &stubLister{items: map[string][]*unstructured.Unstructured{
applications: {healthy("web", "a")},
appProjects: {app("default", nil), app("infra", nil)},
}}
found := build(t, lister, catalog(appDescriptor(), projectDescriptor()))
if len(found.Projects) != 2 {
t.Fatalf("projects = %+v, want two", found.Projects)
}
if found.Projects[0].Name != "default" || found.Projects[1].Name != "infra" {
t.Fatalf("projects came back out of order: %+v", found.Projects)
}
if found.Projects[0].Kind != "AppProject" {
t.Fatalf("kind = %q", found.Projects[0].Kind)
}
if len(found.Apps) != 1 {
t.Fatalf("apps = %+v, want the application", found.Apps)
}
}
func TestProjectsAreNotParentsOfApplications(t *testing.T) {
child := healthy("web", "a")
child.SetLabels(map[string]string{trackingLabel: "default"})
lister := &stubLister{items: map[string][]*unstructured.Unstructured{
applications: {child},
appProjects: {app("default", nil)},
}}
found := build(t, lister, catalog(appDescriptor(), projectDescriptor()))
if found.Apps[0].Owner != "" {
t.Fatalf("owner = %q, want the project ignored", found.Apps[0].Owner)
}
}
func TestOnlyArgoTypesAreWarmed(t *testing.T) {
lister := onlyApps(healthy("web", "a"))
build(t, lister, catalog(appDescriptor(), setDescriptor()))
for _, desc := range lister.warmed {
if desc.Group != Group {
t.Fatalf("warmed %s, which is not argo", desc.Resource)
}
}
if len(lister.warmed) != 2 {
t.Fatalf("warmed %d types, want applications and applicationsets", len(lister.warmed))
}
}
func TestInstalledFollowsTheApplicationType(t *testing.T) {
if Installed(catalog()) {
t.Fatal("argo was reported on a cluster without it")
}
if !Installed(catalog(appDescriptor())) {
t.Fatal("argo was not reported on a cluster with applications")
}
}
func TestOnlyTheThreeArgoKindsAreWanted(t *testing.T) {
if listed("rollouts") {
t.Fatal("rollouts is listed; only applications, sets and projects are")
}
if !listed(applications) || !listed(applicationSets) || !listed(appProjects) {
t.Fatal("one of the three argo kinds went missing")
}
}
func TestASourceIsReadFromTheListWhenThereIsNoSingleOne(t *testing.T) {
item := &unstructured.Unstructured{Object: map[string]any{
"spec": map[string]any{
"sources": []any{
"not a source",
map[string]any{"repoURL": ""},
map[string]any{"repoURL": "https://example.com/apps.git"},
},
},
}}
if got := sourceOf(item, "repoURL"); got != "https://example.com/apps.git" {
t.Fatalf("source = %q, want the first usable one in the list", got)
}
}
func TestASourceIsEmptyWhenNoneOfTheListHasIt(t *testing.T) {
item := &unstructured.Unstructured{Object: map[string]any{
"spec": map[string]any{"sources": []any{map[string]any{"path": "apps"}}},
}}
if got := sourceOf(item, "repoURL"); got != "" {
t.Fatalf("source = %q, want empty", got)
}
}
func TestATrackingIDOnlyNamesSomebodyElse(t *testing.T) {
cases := []struct {
name string
id string
self string
want string
}{
{name: "nothing tracked", id: "", self: "guestbook", want: ""},
{name: "no owner in the id", id: "guestbook", self: "guestbook", want: ""},
{name: "tracked by itself", id: "guestbook:apps/Deployment", self: "guestbook", want: ""},
{name: "tracked by another app", id: "root:apps/Deployment", self: "guestbook", want: "root"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := fromTrackingID(tc.id, tc.self); got != tc.want {
t.Fatalf("owner = %q, want %q", got, tc.want)
}
})
}
}
func TestTheDashboardNamesHowAnApplicationAutomates(t *testing.T) {
app := automating(newApplication(), map[string]any{"prune": true, "selfHeal": true})
desc := api.ResourceDescriptor{Group: Group, Version: "v1alpha1", Resource: applications, Kind: "Application"}
if got := automationOf(app, desc); got != "auto · prune, self-heal" {
t.Fatalf("automation = %q", got)
}
}
func TestTheDashboardCallsAPausedApplicationSuspended(t *testing.T) {
app := automating(newApplication(), map[string]any{"enabled": false})
desc := api.ResourceDescriptor{Group: Group, Version: "v1alpha1", Resource: applications, Kind: "Application"}
if got := automationOf(app, desc); got != "suspended · neither prune nor self-heal" {
t.Fatalf("automation = %q", got)
}
}
func TestTheDashboardCallsAnApplicationWithNoPolicyManual(t *testing.T) {
desc := api.ResourceDescriptor{Group: Group, Version: "v1alpha1", Resource: applications, Kind: "Application"}
if got := automationOf(newApplication(), desc); got != "manual" {
t.Fatalf("automation = %q", got)
}
}
func TestTheDashboardLeavesProjectsAndSetsWithoutAPolicy(t *testing.T) {
desc := api.ResourceDescriptor{Group: Group, Version: "v1alpha1", Resource: appProjects, Kind: "AppProject"}
if got := automationOf(newApplication(), desc); got != "" {
t.Fatalf("automation = %q, want nothing", got)
}
}