From d32e83ab51f7d360ab54dac31621c82e8a3e6acd Mon Sep 17 00:00:00 2001 From: Xavi Garcia Date: Tue, 30 Jun 2026 14:47:35 +0200 Subject: [PATCH] Adds basic skeleton and tests for agentmanagement This PR adds basic testing coverage skeleton and tests very basic things done by the agentmanagement component. Refers to: https://github.com/rancher/fleet/issues/5372 Signed-off-by: Xavi Garcia --- .../agentmanagement/helpers_test.go | 39 ++++++ .../agentmanagement/resources_test.go | 127 ++++++++++++++++++ .../agentmanagement/smoke_test.go | 46 +++++++ .../agentmanagement/suite_test.go | 100 ++++++++++++++ 4 files changed, 312 insertions(+) create mode 100644 integrationtests/agentmanagement/helpers_test.go create mode 100644 integrationtests/agentmanagement/resources_test.go create mode 100644 integrationtests/agentmanagement/smoke_test.go create mode 100644 integrationtests/agentmanagement/suite_test.go diff --git a/integrationtests/agentmanagement/helpers_test.go b/integrationtests/agentmanagement/helpers_test.go new file mode 100644 index 0000000000..4adf2c7e64 --- /dev/null +++ b/integrationtests/agentmanagement/helpers_test.go @@ -0,0 +1,39 @@ +package agentmanagement_test + +import ( + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func newNamespace(name string) *corev1.Namespace { + return &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + } +} + +// clusterRole returns a ClusterRole with the given name (cluster-scoped). +func clusterRole(name string) *rbacv1.ClusterRole { + return &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + } +} + +// objectExists returns an AsyncAssertion that succeeds when the object can be +// fetched from the API server. +func objectExists(obj client.Object) AsyncAssertion { + key := types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()} + return Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, key, obj)).To(Succeed()) + }) +} + +// namespaceExists returns an AsyncAssertion that succeeds when a namespace with +// the given name exists. +func namespaceExists(name string) AsyncAssertion { + return objectExists(newNamespace(name)) +} diff --git a/integrationtests/agentmanagement/resources_test.go b/integrationtests/agentmanagement/resources_test.go new file mode 100644 index 0000000000..c41e0d5975 --- /dev/null +++ b/integrationtests/agentmanagement/resources_test.go @@ -0,0 +1,127 @@ +package agentmanagement_test + +// Tests for internal/cmd/controller/agentmanagement/controllers/resources. +// +// ApplyBootstrapResources is a one-shot synchronous call made during +// controllers.Register (no watch loop). The frozen contract asserts: +// - exact PolicyRule content on both ClusterRoles +// - both namespaces created +// - objects are stable (Consistently) — no lifecycle management after startup +// +// No-prune path: the object set is fixed (always the same 4 objects); +// there is no varying input that would trigger GC of an orphan. Wrangler +// apply idempotency is exercised by the suite running a second time on a +// fresh envtest (objects re-created cleanly on each run). + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/rancher/fleet/internal/cmd/controller/agentmanagement/controllers/resources" + fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("resources.ApplyBootstrapResources", func() { + // registrationNS is derived from systemNamespace per fleetns.SystemRegistrationNamespace. + const registrationNS = "cattle-fleet-clusters-system" + + Describe("system and registration namespaces", func() { + It("creates the system namespace", func() { + namespaceExists(systemNamespace).Should(Succeed()) + }) + + It("creates the system registration namespace", func() { + namespaceExists(registrationNS).Should(Succeed()) + }) + + It("keeps both namespaces stable (no lifecycle management after startup)", func() { + Consistently(func(g Gomega) { + ns := &corev1.Namespace{} + g.Expect(k8sClient.Get(ctx, + types.NamespacedName{Name: systemNamespace}, ns)).To(Succeed()) + g.Expect(k8sClient.Get(ctx, + types.NamespacedName{Name: registrationNS}, ns)).To(Succeed()) + }).Should(Succeed()) + }) + }) + + Describe("fleet-bundle-deployment ClusterRole", func() { + It("exists", func() { + objectExists(clusterRole(resources.BundleDeploymentClusterRole)).Should(Succeed()) + }) + + It("has exactly the expected policy rules", func() { + cr := &rbacv1.ClusterRole{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: resources.BundleDeploymentClusterRole, + }, cr)).To(Succeed()) + }).Should(Succeed()) + + Expect(cr.Rules).To(ConsistOf( + rbacv1.PolicyRule{ + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{fleet.SchemeGroupVersion.Group}, + Resources: []string{fleet.BundleDeploymentResourceNamePlural}, + }, + rbacv1.PolicyRule{ + Verbs: []string{"update", "patch"}, + APIGroups: []string{fleet.SchemeGroupVersion.Group}, + Resources: []string{fleet.BundleDeploymentResourceNamePlural + "/status"}, + }, + rbacv1.PolicyRule{ + Verbs: []string{"get"}, + APIGroups: []string{""}, + Resources: []string{"secrets", "configmaps"}, + }, + )) + }) + + It("is stable (rules do not drift)", func() { + Consistently(func(g Gomega) { + cr := &rbacv1.ClusterRole{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: resources.BundleDeploymentClusterRole, + }, cr)).To(Succeed()) + g.Expect(cr.Rules).To(HaveLen(3)) + }).Should(Succeed()) + }) + }) + + Describe("fleet-content ClusterRole", func() { + It("exists", func() { + objectExists(clusterRole(resources.ContentClusterRole)).Should(Succeed()) + }) + + It("has exactly the expected policy rule", func() { + cr := &rbacv1.ClusterRole{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: resources.ContentClusterRole, + }, cr)).To(Succeed()) + }).Should(Succeed()) + + Expect(cr.Rules).To(ConsistOf( + rbacv1.PolicyRule{ + Verbs: []string{"get"}, + APIGroups: []string{fleet.SchemeGroupVersion.Group}, + Resources: []string{fleet.ContentResourceNamePlural}, + }, + )) + }) + + It("is stable (rules do not drift)", func() { + Consistently(func(g Gomega) { + cr := &rbacv1.ClusterRole{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: resources.ContentClusterRole, + }, cr)).To(Succeed()) + g.Expect(cr.Rules).To(HaveLen(1)) + }).Should(Succeed()) + }) + }) +}) diff --git a/integrationtests/agentmanagement/smoke_test.go b/integrationtests/agentmanagement/smoke_test.go new file mode 100644 index 0000000000..b711b22178 --- /dev/null +++ b/integrationtests/agentmanagement/smoke_test.go @@ -0,0 +1,46 @@ +package agentmanagement_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/rancher/fleet/internal/cmd/controller/agentmanagement/controllers/resources" + "github.com/rancher/fleet/internal/config" +) + +var _ = Describe("AgentManagement harness smoke test", func() { + It("starts the controllers without error", func() { + // Reaching here means BeforeSuite completed — envtest started, + // Wrangler controllers registered and started. + Expect(true).To(BeTrue()) + }) + + It("sets global config after controller startup", func() { + // config.Register (called inside controllers.Register) does an + // initial config.Lookup and calls config.SetAndTrigger so + // config.Get() must return a non-nil value here. + Expect(config.Get()).NotTo(BeNil()) + }) + + It("creates the system namespace via ApplyBootstrapResources", func() { + // resources.ApplyBootstrapResources runs synchronously in Register. + namespaceExists(systemNamespace).Should(Succeed()) + }) + + It("creates the system registration namespace via ApplyBootstrapResources", func() { + // The registration namespace is derived from the system namespace: + // "cattle-fleet-system" → "cattle-fleet-clusters-system". + regNS := "cattle-fleet-clusters-system" + namespaceExists(regNS).Should(Succeed()) + }) + + It("creates the fleet-bundle-deployment ClusterRole", func() { + cr := clusterRole(resources.BundleDeploymentClusterRole) + objectExists(cr).Should(Succeed()) + }) + + It("creates the fleet-content ClusterRole", func() { + cr := clusterRole(resources.ContentClusterRole) + objectExists(cr).Should(Succeed()) + }) +}) diff --git a/integrationtests/agentmanagement/suite_test.go b/integrationtests/agentmanagement/suite_test.go new file mode 100644 index 0000000000..fea9345bd8 --- /dev/null +++ b/integrationtests/agentmanagement/suite_test.go @@ -0,0 +1,100 @@ +// Package agentmanagement_test contains behavioral (envtest) integration tests +// for the agentmanagement controllers. They exercise the controllers against a +// real API server and assert on the resulting cluster state. +// +// # Running +// +// KUBEBUILDER_ASSETS=$(setup-envtest use --use-env -p path 1.34) \ +// ginkgo ./integrationtests/agentmanagement/... +// +// # Coverage +// +// go test -coverprofile=cover.out \ +// -coverpkg=github.com/rancher/fleet/internal/cmd/controller/agentmanagement/... \ +// github.com/rancher/fleet/integrationtests/agentmanagement +package agentmanagement_test + +import ( + "context" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/rancher/wrangler/v3/pkg/schemes" + + "github.com/rancher/fleet/integrationtests/utils" + "github.com/rancher/fleet/internal/cmd/controller/agentmanagement/controllers" + "github.com/rancher/fleet/internal/config" + + appsv1 "k8s.io/api/apps/v1" + policyv1 "k8s.io/api/policy/v1" + schedulingv1 "k8s.io/api/scheduling/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" +) + +var ( + cancel context.CancelFunc + cfg *rest.Config + ctx context.Context + k8sClient client.Client + testenv *envtest.Environment +) + +// systemNamespace is the Fleet controller namespace used across all specs. +const systemNamespace = "cattle-fleet-system" + +func TestFleet(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Fleet AgentManagement Suite") +} + +var _ = BeforeSuite(func() { + utils.SuppressLogs() + ctx, cancel = context.WithCancel(context.TODO()) + + // CRDs are two levels above this package: + // integrationtests/agentmanagement/ → ../.. → repo root + testenv = utils.NewEnvTest("../..") + + var err error + cfg, err = utils.StartTestEnv(testenv) + Expect(err).NotTo(HaveOccurred()) + + k8sClient, err = utils.NewClient(cfg) + Expect(err).NotTo(HaveOccurred()) + + // Register additional types that manageagent apply-objects need + // (mirrors what production start.go registers via schemes.Register). + Expect(schemes.Register(appsv1.AddToScheme)).To(Succeed()) + Expect(schemes.Register(policyv1.AddToScheme)).To(Succeed()) + Expect(schemes.Register(schedulingv1.AddToScheme)).To(Succeed()) + + // Seed global config before any controller fires so config.Get() never panics. + Expect(config.SetAndTrigger(config.DefaultConfig())).To(Succeed()) + + // Build a clientcmd.ClientConfig wrapping the envtest *rest.Config so + // controllers.NewAppContext (which calls cfg.ClientConfig() internally) + // can use it. utils.FromEnvTestConfig serialises the cert material into + // a valid kubeconfig; no production seam is needed. + kubeconfigBytes := utils.FromEnvTestConfig(cfg) + clientCfg, err := clientcmd.NewClientConfigFromBytes(kubeconfigBytes) + Expect(err).NotTo(HaveOccurred()) + + appCtx, err := controllers.NewAppContext(clientCfg) + Expect(err).NotTo(HaveOccurred()) + + // Register and start the agentmanagement controllers, with bootstrap + // disabled so the suite does not require local-cluster wiring. + err = controllers.Register(ctx, appCtx, systemNamespace, + true, /* disableBootstrap */ + true /* enforceTTL */) + Expect(err).NotTo(HaveOccurred()) +}) + +var _ = AfterSuite(func() { + cancel() + Expect(testenv.Stop()).ToNot(HaveOccurred()) +})