Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions integrationtests/agentmanagement/helpers_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
127 changes: 127 additions & 0 deletions integrationtests/agentmanagement/resources_test.go
Original file line number Diff line number Diff line change
@@ -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())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this redundant with the smoke tests?

})

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())
})
})
})
46 changes: 46 additions & 0 deletions integrationtests/agentmanagement/smoke_test.go
Original file line number Diff line number Diff line change
@@ -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())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comment; I was confused here for a bit 😅

})

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.
Comment on lines +19 to +21

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except config.SetAndTrigger(config.DefaultConfig() is now called explicitly from BeforeSuite, so I'm not sure what this test case brings 🤔

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())
})
})
100 changes: 100 additions & 0 deletions integrationtests/agentmanagement/suite_test.go
Original file line number Diff line number Diff line change
@@ -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())
})