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
202 changes: 202 additions & 0 deletions integrationtests/agentmanagement/cluster_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package agentmanagement_test

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)

var _ = Describe("cluster namespace lifecycle", func() {
var regNamespace string

BeforeEach(func() {
ns := newGeneratedNamespace("cluster-controller-test-")
Expect(k8sClient.Create(ctx, ns)).To(Succeed())
regNamespace = ns.Name
})

It("sets status.Namespace to the documented hash and creates the namespace with the managed label and annotations", func() {
cluster := newCluster(regNamespace, "test-cluster")
Expect(k8sClient.Create(ctx, cluster)).To(Succeed())

expectedNS := clusterNamespaceName(regNamespace, "test-cluster")
Eventually(func(g Gomega) {
c := &fleet.Cluster{}
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: regNamespace, Name: "test-cluster"}, c)).To(Succeed())
g.Expect(c.Status.Namespace).To(Equal(expectedNS))
}).Should(Succeed())

Eventually(func(g Gomega) {
ns := &corev1.Namespace{}
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: expectedNS}, ns)).To(Succeed())
g.Expect(ns.Labels).To(HaveKeyWithValue(fleet.ManagedLabel, "true"))
g.Expect(ns.Annotations).To(HaveKeyWithValue(fleet.ClusterNamespaceAnnotation, regNamespace))
g.Expect(ns.Annotations).To(HaveKeyWithValue(fleet.ClusterAnnotation, "test-cluster"))
}).Should(Succeed())
})

It("does not create a namespace for a cluster carrying a custom cluster-management label", func() {
cluster := newCluster(regNamespace, "skip-cluster")
cluster.Labels = map[string]string{fleet.ClusterManagementLabel: "custom-manager"}
Expect(k8sClient.Create(ctx, cluster)).To(Succeed())

Eventually(func(g Gomega) {
c := &fleet.Cluster{}
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: regNamespace, Name: "skip-cluster"}, c)).To(Succeed())
g.Expect(c.Status.Conditions).To(ContainElement(HaveField("Type", fleet.ClusterConditionProcessed)))
}).Should(Succeed())

expectedNS := clusterNamespaceName(regNamespace, "skip-cluster")
Consistently(func(g Gomega) {
c := &fleet.Cluster{}
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: regNamespace, Name: "skip-cluster"}, c)).To(Succeed())
g.Expect(c.Status.Namespace).To(BeEmpty())

err := k8sClient.Get(ctx, types.NamespacedName{Name: expectedNS}, &corev1.Namespace{})
g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
}).Should(Succeed())
})

It("does not overwrite a namespace that already exists at the generated name", func() {
clusterName := "preexisting-ns-cluster"
expectedNS := clusterNamespaceName(regNamespace, clusterName)

preexisting := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: expectedNS,
Labels: map[string]string{"owner": "someone-else"},
},
}
Expect(k8sClient.Create(ctx, preexisting)).To(Succeed())

cluster := newCluster(regNamespace, clusterName)
Expect(k8sClient.Create(ctx, cluster)).To(Succeed())

Eventually(func(g Gomega) {
c := &fleet.Cluster{}
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: regNamespace, Name: clusterName}, c)).To(Succeed())
g.Expect(c.Status.Namespace).To(Equal(expectedNS))
}).Should(Succeed())

Consistently(func(g Gomega) {
ns := &corev1.Namespace{}
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: expectedNS}, ns)).To(Succeed())
g.Expect(ns.Labels).To(HaveKeyWithValue("owner", "someone-else"))
g.Expect(ns.Labels).NotTo(HaveKey(fleet.ManagedLabel))
}).Should(Succeed())
})

It("does not delete or modify the generated namespace itself when the owning cluster is deleted", func() {
clusterName := "deleted-cluster"
cluster := newCluster(regNamespace, clusterName)
Expect(k8sClient.Create(ctx, cluster)).To(Succeed())

expectedNS := clusterNamespaceName(regNamespace, clusterName)
namespaceExists(expectedNS).Should(Succeed())

Expect(k8sClient.Delete(ctx, cluster)).To(Succeed())
Eventually(func(g Gomega) {
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: regNamespace, Name: clusterName}, &fleet.Cluster{})
g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
}).Should(Succeed())

Consistently(func(g Gomega) {
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: expectedNS}, &corev1.Namespace{})).To(Succeed())
}).Should(Succeed())
})

Describe("bundledeployment changes", func() {
It("does not error or create any namespace when a BundleDeployment changes in a namespace without cluster annotations", func() {

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.

Could we check that the opposite also works? i.e. a namespace may be created when a bundle deployment changes in a namespace with cluster annotations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch!. Added a test for the positive branch — and it exposed that my existing annotated-namespace spec couldn't fail anyway: the cluster namespace already exists when the BundleDeployment is created, so the watch firing has no observable effect.

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 adding this new test case :)

and it exposed that my existing annotated-namespace spec couldn't fail anyway: the cluster namespace already exists when the BundleDeployment is created, so the watch firing has no observable effect.

Does this mean that the negative test case should delete the cluster namespace, as the positive test case does, to ensure that the watch is fired?

plainNS := newGeneratedNamespace("no-cluster-annotations-")
Expect(k8sClient.Create(ctx, plainNS)).To(Succeed())

existingNamespaces := &corev1.NamespaceList{}
Expect(k8sClient.List(ctx, existingNamespaces)).To(Succeed())
existingNames := make(map[string]bool, len(existingNamespaces.Items))
for _, ns := range existingNamespaces.Items {
existingNames[ns.Name] = true
}

bd := newBundleDeployment(plainNS.Name, "orphan-bd")
Expect(k8sClient.Create(ctx, bd)).To(Succeed())

Consistently(func(g Gomega) {
g.Expect(k8sClient.Get(ctx,
types.NamespacedName{Namespace: plainNS.Name, Name: "orphan-bd"},
&fleet.BundleDeployment{})).To(Succeed())

currentNamespaces := &corev1.NamespaceList{}
g.Expect(k8sClient.List(ctx, currentNamespaces)).To(Succeed())
for _, ns := range currentNamespaces.Items {
g.Expect(existingNames).To(HaveKey(ns.Name), "unexpected namespace created: %s", ns.Name)
}
}).Should(Succeed())
})

It("re-creates the cluster namespace when a BundleDeployment changes in a namespace with cluster annotations", func() {
clusterName := "bd-requeue-cluster"
expectedNS := clusterNamespaceName(regNamespace, clusterName)

// A namespace annotated for the cluster, holding the BundleDeployment.
// It is created first so the controller's namespace cache is warm by
// the time the BundleDeployment triggers the watch.
annotatedNS := newGeneratedNamespace("cluster-annotated-")
annotatedNS.Annotations = map[string]string{
fleet.ClusterNamespaceAnnotation: regNamespace,
fleet.ClusterAnnotation: clusterName,
}
Expect(k8sClient.Create(ctx, annotatedNS)).To(Succeed())

cluster := newCluster(regNamespace, clusterName)
Expect(k8sClient.Create(ctx, cluster)).To(Succeed())
namespaceExists(expectedNS).Should(Succeed())

// Drop the generated namespace, so that only a further reconcile of the
// cluster can bring it back. Deleting it does not enqueue the cluster.
deleteNamespace(expectedNS)
Consistently(func(g Gomega) {
err := k8sClient.Get(ctx, types.NamespacedName{Name: expectedNS}, &corev1.Namespace{})
g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
}).Should(Succeed())

bd := newBundleDeployment(annotatedNS.Name, "requeue-bd")
Expect(k8sClient.Create(ctx, bd)).To(Succeed())

Eventually(func(g Gomega) {
ns := &corev1.Namespace{}
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: expectedNS}, ns)).To(Succeed())
g.Expect(ns.Labels).To(HaveKeyWithValue(fleet.ManagedLabel, "true"))
g.Expect(ns.Annotations).To(HaveKeyWithValue(fleet.ClusterNamespaceAnnotation, regNamespace))
g.Expect(ns.Annotations).To(HaveKeyWithValue(fleet.ClusterAnnotation, clusterName))
}).Should(Succeed())
})

It("does not corrupt the owning cluster's generated namespace or status when a BundleDeployment is created inside it", func() {
clusterName := "bd-trigger-cluster"
cluster := newCluster(regNamespace, clusterName)
Expect(k8sClient.Create(ctx, cluster)).To(Succeed())

expectedNS := clusterNamespaceName(regNamespace, clusterName)
namespaceExists(expectedNS).Should(Succeed())

bd := newBundleDeployment(expectedNS, "agent-bd")
Expect(k8sClient.Create(ctx, bd)).To(Succeed())

Consistently(func(g Gomega) {
c := &fleet.Cluster{}
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: regNamespace, Name: clusterName}, c)).To(Succeed())
g.Expect(c.Status.Namespace).To(Equal(expectedNS))

ns := &corev1.Namespace{}
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: expectedNS}, ns)).To(Succeed())
g.Expect(ns.Labels).To(HaveKeyWithValue(fleet.ManagedLabel, "true"))
}).Should(Succeed())
})
})
})
105 changes: 105 additions & 0 deletions integrationtests/agentmanagement/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package agentmanagement_test

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/rancher/fleet/internal/config"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
)

var _ = Describe("config ConfigMap watch", Ordered, func() {
key := types.NamespacedName{Namespace: systemNamespace, Name: config.ManagerConfigName}

BeforeAll(func() {
DeferCleanup(func() {
cm := &corev1.ConfigMap{}
defaultCM := newConfigMap(systemNamespace, config.ManagerConfigName, config.DefaultConfig())
err := k8sClient.Get(ctx, key, cm)
switch {
case apierrors.IsNotFound(err):
Expect(k8sClient.Create(ctx, defaultCM)).To(Succeed())
case err == nil:
cm.Data = defaultCM.Data
Expect(k8sClient.Update(ctx, cm)).To(Succeed())
default:
Expect(err).NotTo(HaveOccurred())
}

Eventually(func(g Gomega) {
g.Expect(config.Get()).To(Equal(config.DefaultConfig()))
}).Should(Succeed())
})
})

It("reflects a newly created manager ConfigMap in config.Get()", func() {
cfg := config.DefaultConfig()
cfg.AgentImage = "rancher/fleet-agent:config-test-created"
cfg.IgnoreClusterRegistrationLabels = true

Expect(k8sClient.Create(ctx, newConfigMap(systemNamespace, config.ManagerConfigName, cfg))).To(Succeed())

Eventually(func(g Gomega) {
g.Expect(config.Get().AgentImage).To(Equal("rancher/fleet-agent:config-test-created"))
g.Expect(config.Get().IgnoreClusterRegistrationLabels).To(BeTrue())
}).Should(Succeed())
})

It("reflects an updated manager ConfigMap in config.Get()", func() {
cfg := config.DefaultConfig()
cfg.AgentImage = "rancher/fleet-agent:config-test-updated"
cfg.IgnoreClusterRegistrationLabels = false

cm := &corev1.ConfigMap{}
Expect(k8sClient.Get(ctx, key, cm)).To(Succeed())

updated := newConfigMap(systemNamespace, config.ManagerConfigName, cfg)
cm.Data = updated.Data
Expect(k8sClient.Update(ctx, cm)).To(Succeed())

Eventually(func(g Gomega) {
g.Expect(config.Get().AgentImage).To(Equal("rancher/fleet-agent:config-test-updated"))
g.Expect(config.Get().IgnoreClusterRegistrationLabels).To(BeFalse())
}).Should(Succeed())
})

It("treats deletion of the manager ConfigMap as a no-op, retaining the last known config", func() {
cm := &corev1.ConfigMap{}
Expect(k8sClient.Get(ctx, key, cm)).To(Succeed())
Expect(k8sClient.Delete(ctx, cm)).To(Succeed())

Eventually(func(g Gomega) {
err := k8sClient.Get(ctx, key, &corev1.ConfigMap{})
g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
}).Should(Succeed())

Consistently(func(g Gomega) {
g.Expect(config.Get().AgentImage).To(Equal("rancher/fleet-agent:config-test-updated"))
}).Should(Succeed())
})

It("ignores a ConfigMap with the right name in the wrong namespace", func() {
cfg := config.DefaultConfig()
cfg.AgentImage = "rancher/fleet-agent:should-be-ignored-namespace"

Expect(k8sClient.Create(ctx, newConfigMap("default", config.ManagerConfigName, cfg))).To(Succeed())

Consistently(func(g Gomega) {
g.Expect(config.Get().AgentImage).To(Equal("rancher/fleet-agent:config-test-updated"))
}).Should(Succeed())
})

It("ignores a ConfigMap with the wrong name in the system namespace", func() {
cfg := config.DefaultConfig()
cfg.AgentImage = "rancher/fleet-agent:should-be-ignored-name"

Expect(k8sClient.Create(ctx, newConfigMap(systemNamespace, "not-the-manager-config", cfg))).To(Succeed())

Consistently(func(g Gomega) {
g.Expect(config.Get().AgentImage).To(Equal("rancher/fleet-agent:config-test-updated"))
}).Should(Succeed())
})
})
76 changes: 76 additions & 0 deletions integrationtests/agentmanagement/helpers_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package agentmanagement_test

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/rancher/fleet/internal/config"
"github.com/rancher/fleet/internal/names"
fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"

corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -37,3 +43,73 @@ func objectExists(obj client.Object) AsyncAssertion {
func namespaceExists(name string) AsyncAssertion {
return objectExists(newNamespace(name))
}

// namespaceIsGone returns an AsyncAssertion that succeeds when no namespace with
// the given name exists.
func namespaceIsGone(name string) AsyncAssertion {
return Eventually(func(g Gomega) {
err := k8sClient.Get(ctx, types.NamespacedName{Name: name}, &corev1.Namespace{})
g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
})
}

// deleteNamespace removes a namespace for good. envtest runs no namespace
// controller, so a deleted namespace stays Terminating forever unless the
// kubernetes finalizer is cleared through the finalize subresource.
func deleteNamespace(name string) {
GinkgoHelper()

ns := newNamespace(name)
Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, ns))).To(Succeed())

Eventually(func(g Gomega) {
err := k8sClient.Get(ctx, types.NamespacedName{Name: name}, ns)
if apierrors.IsNotFound(err) {
return
}
g.Expect(err).NotTo(HaveOccurred())

ns.Spec.Finalizers = nil
g.Expect(client.IgnoreNotFound(k8sClient.SubResource("finalize").Update(ctx, ns))).To(Succeed())
}).Should(Succeed())

namespaceIsGone(name).Should(Succeed())
}

// newConfigMap builds the ConfigMap representation of cfg, as expected by the
// global config's ConfigMap watch.
func newConfigMap(namespace, name string, cfg *config.Config) *corev1.ConfigMap {
cm, err := config.ToConfigMap(namespace, name, cfg)
Expect(err).NotTo(HaveOccurred())
return cm
}

// newGeneratedNamespace returns a namespace object whose actual name is
// assigned by the API server on creation, using prefix as the GenerateName.
func newGeneratedNamespace(prefix string) *corev1.Namespace {
return &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{GenerateName: prefix},
}
}

// newCluster returns a minimal Cluster in the given namespace.
func newCluster(namespace, name string) *fleet.Cluster {
return &fleet.Cluster{
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name},
}
}

// clusterNamespaceName predicts the generated cluster namespace name for a
// Cluster identified by its (namespace, name), following the documented
// format of Cluster.Status.Namespace: "cluster-<namespace>-<name>-<hash>".
func clusterNamespaceName(namespace, name string) string {
return names.SafeConcatName("cluster", namespace, name,
names.KeyHash(namespace+"::"+name))
}

// newBundleDeployment returns a minimal BundleDeployment in the given namespace.
func newBundleDeployment(namespace, name string) *fleet.BundleDeployment {
return &fleet.BundleDeployment{
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name},
}
}