Skip to content
This repository has been archived by the owner on Jun 26, 2023. It is now read-only.

Create a default HNCConfiguration singleton when starting HNC if the singleton does not exist #453

Closed
Closed
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
12 changes: 12 additions & 0 deletions incubator/hnc/pkg/reconcilers/hierarchy_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ type HierarchyConfigReconciler struct {
// were part of the same reconciliation attempt, even if multiple are running parallel (or it's
// simply hard to tell when one ends and another begins).
reconcileID int32

// This is a temporary field to toggle different behaviours of this HierarchyConfigurationReconciler
// depending on if the HierarchicalNamespaceReconciler is enabled or not. It will be removed after
// the GitHub issue "Implement self-service namespace" is resolved
// (https://github.com/kubernetes-sigs/multi-tenancy/issues/457)
HNSReconcilerEnabled bool
}

// +kubebuilder:rbac:groups=hnc.x-k8s.io,resources=hierarchies,verbs=get;list;watch;create;update;patch;delete
Expand All @@ -83,6 +89,12 @@ func (r *HierarchyConfigReconciler) Reconcile(req ctrl.Request) (ctrl.Result, er

rid := (int)(atomic.AddInt32(&r.reconcileID, 1))
log := r.Log.WithValues("ns", ns, "rid", rid)

// TODO remove this log and use the HNSReconcilerEnabled to toggle the behavour of this
// reconciler accordingly. See issue: https://github.com/kubernetes-sigs/multi-tenancy/issues/467
// Output a log for testing.
log.Info("HC will be reconciled with", "HNSReconcilerEnabled", r.HNSReconcilerEnabled)

return ctrl.Result{}, r.reconcile(ctx, log, ns)
}

Expand Down
42 changes: 39 additions & 3 deletions incubator/hnc/pkg/reconcilers/hnc_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"

"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/source"

"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/api/errors"
Expand All @@ -29,6 +31,11 @@ type ConfigReconciler struct {
// Forest is the in-memory data structure that is shared with all other reconcilers.
Forest *forest.Forest

// Igniter is a channel of event.GenericEvent (see "Watching Channels" in
// https://book-v1.book.kubebuilder.io/beyond_basics/controller_watches.html)
// that is used to enqueue the singleton for initial reconciliation.
Igniter chan event.GenericEvent

// HierarchyConfigUpdates is a channel of events used to update hierarchy configuration changes performed by
// ObjectReconcilers. It is passed on to ObjectReconcilers for the updates. The ConfigReconciler itself does
// not use it.
Expand Down Expand Up @@ -56,7 +63,7 @@ func (r *ConfigReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
// achieve the reconciliation.
r.Log.Info("Reconciling cluster-wide HNC configuration.")

// Create cooresponding ObjectReconcilers for newly added types, if needed.
// Create corresponding ObjectReconcilers for newly added types, if needed.
// TODO: Returning an error here will make controller-runtime to requeue the object to be reconciled again.
// A better way to handle this is to display the error as part of the HNCConfig.status field to suggest the error is a
// permanent problem and to stop controller-runtime from retrying.
Expand Down Expand Up @@ -171,9 +178,38 @@ func (r *ConfigReconciler) createObjectReconciler(gvk schema.GroupVersionKind) e
return nil
}

// forceInitialReconcile forces reconciliation to start after setting up the
// controller with the manager. This is used to create a default singleton if
// there is no singleton in the cluster. This occurs in a goroutine so the
// caller doesn't block; since the reconciler is never garbage-collected,
// this is safe.
func (r *ConfigReconciler) forceInitialReconcile(log logr.Logger, reason string) {
go func() {
log.Info("Enqueuing for reconciliation", "reason", reason)
// The watch handler doesn't care about anything except the metadata.
inst := &api.HNCConfiguration{}
inst.ObjectMeta.Name = api.HNCConfigSingleton
r.Igniter <- event.GenericEvent{Meta: inst}
}()
}

// SetupWithManager builds a controller with the reconciler.
func (r *ConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
if err := ctrl.NewControllerManagedBy(mgr).
Copy link
Contributor

Choose a reason for hiding this comment

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

Usually we'd put these in separate statements; the if err := foo(); err != nil pattern really only works as a one-liner. But I'll approve anyway :)

For(&api.HNCConfiguration{}).
Complete(r)
Watches(&source.Channel{Source: r.Igniter}, &handler.EnqueueRequestForObject{}).
Complete(r); err != nil {
return err
}
// Create a default singleton if there is no singleton in the cluster.
//
// The cache used by the client to retrieve objects might not be populated
// at this point. As a result, we cannot use r.Get() to determine the existence
// of the singleton and then use r.Create() to create the singleton if
// it does not exist. As a workaround, we decide to enforce reconciliation. The
// cache is populated at the reconciliation stage. A default singleton will be
// created during the reconciliation if there is no singleton in the cluster.
r.forceInitialReconcile(r.Log, "Enforce reconciliation to create a default"+
"HNCConfiguration singleton if it does not exist.")
return nil
}
124 changes: 107 additions & 17 deletions incubator/hnc/pkg/reconcilers/hnc_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package reconcilers_test

import (
"context"
"time"

api "github.com/kubernetes-sigs/multi-tenancy/incubator/hnc/api/v1alpha1"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)

Expand All @@ -22,6 +23,11 @@ var _ = Describe("HNCConfiguration", func() {
BeforeEach(func() {
fooName = createNS(ctx, "foo")
barName = createNS(ctx, "bar")

// Give foo a role.
makeRole(ctx, fooName, "foo-role")
// Give foo a role binding.
makeRoleBinding(ctx, fooName, "foo-role", "foo-admin", "foo-role-binding")
})

AfterEach(func() {
Expand All @@ -31,32 +37,56 @@ var _ = Describe("HNCConfiguration", func() {
}).Should(Succeed())
})

It("should propagate objects whose types have been added to HNCConfiguration", func() {
It("should set mode of Roles and RoleBindings as propagate by default", func() {
config := getHNCConfig(ctx)

Eventually(hasTypeWithMode("rbac.authorization.k8s.io/v1", "Role", api.Propagate, config)).Should(BeTrue())
Eventually(hasTypeWithMode("rbac.authorization.k8s.io/v1", "RoleBinding", api.Propagate, config)).Should(BeTrue())
adrianludwin marked this conversation as resolved.
Show resolved Hide resolved
})

It("should propagate Roles by default", func() {
setParent(ctx, barName, fooName)
makeSecret(ctx, fooName, "foo-sec")

// Wait 1 second to give "foo-sec" a chance to be propagated to bar, if it can be propagated.
time.Sleep(1 * time.Second)
// Foo should have "foo-sec" since we created it there.
Eventually(hasSecret(ctx, fooName, "foo-sec")).Should(BeTrue())
// "foo-sec" is not propagated to bar because Secret hasn't been configured in HNCConfiguration.
Eventually(hasSecret(ctx, barName, "foo-sec")).Should(BeFalse())
Eventually(hasRole(ctx, barName, "foo-role")).Should(BeTrue())
Expect(roleInheritedFrom(ctx, barName, "foo-role")).Should(Equal(fooName))
})

Eventually(func() error {
c := getHNCConfig(ctx)
return addSecretToHNCConfig(ctx, c)
adrianludwin marked this conversation as resolved.
Show resolved Hide resolved
}).Should(Succeed())
It("should propagate RoleBindings by default", func() {
setParent(ctx, barName, fooName)

Eventually(hasRoleBinding(ctx, barName, "foo-role-binding")).Should(BeTrue())
Expect(roleBindingInheritedFrom(ctx, barName, "foo-role-binding")).Should(Equal(fooName))
})

It("should only propagate objects whose types are in HNCConfiguration", func() {
setParent(ctx, barName, fooName)
makeSecret(ctx, fooName, "foo-sec")
makeResourceQuota(ctx, fooName, "foo-resource-quota")

addToHNCConfig(ctx, "v1", "Secret", api.Propagate)

// Foo should have both "foo-sec" and "foo-resource-quota" since we created there.
Eventually(hasSecret(ctx, fooName, "foo-sec")).Should(BeTrue())
Eventually(hasResourceQuota(ctx, fooName, "foo-resource-quota")).Should(BeTrue())
// "foo-sec" should now be propagated from foo to bar.
Eventually(hasSecret(ctx, barName, "foo-sec")).Should(BeTrue())
Expect(secretInheritedFrom(ctx, barName, "foo-sec")).Should(Equal(fooName))
// "foo-resource-quota" should not be propagated from foo to bar because ResourceQuota
// is not added to HNCConfiguration.
Expect(hasResourceQuota(ctx, barName, "foo-resource-quota")).Should(BeFalse())
})
})

func addSecretToHNCConfig(ctx context.Context, c *api.HNCConfiguration) error {
secSpec := api.TypeSynchronizationSpec{APIVersion: "v1", Kind: "Secret", Mode: api.Propagate}
c.Spec.Types = append(c.Spec.Types, secSpec)
return updateHNCConfig(ctx, c)
func hasTypeWithMode(apiVersion, kind string, mode api.SynchronizationMode, config *api.HNCConfiguration) func() bool {
// `Eventually` only works with a fn that doesn't take any args
return func() bool {
for _, t := range config.Spec.Types {
if t.APIVersion == apiVersion && t.Kind == kind && t.Mode == mode {
return true
}
}
return false
}
}

func makeSecret(ctx context.Context, nsName, secretName string) {
Expand Down Expand Up @@ -89,3 +119,63 @@ func secretInheritedFrom(ctx context.Context, nsName, secretName string) string
lif, _ := sec.ObjectMeta.Labels["hnc.x-k8s.io/inheritedFrom"]
return lif
}

func makeRoleBinding(ctx context.Context, nsName, roleName, userName, roleBindingName string) {
roleBinding := &v1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: roleBindingName,
Namespace: nsName,
},
Subjects: []v1.Subject{
{
Kind: "User",
Name: userName,
},
},
RoleRef: v1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: roleName,
},
}
ExpectWithOffset(1, k8sClient.Create(ctx, roleBinding)).Should(Succeed())
}

func hasRoleBinding(ctx context.Context, nsName, roleBindingName string) func() bool {
// `Eventually` only works with a fn that doesn't take any args
return func() bool {
nnm := types.NamespacedName{Namespace: nsName, Name: roleBindingName}
roleBinding := &v1.RoleBinding{}
err := k8sClient.Get(ctx, nnm, roleBinding)
return err == nil
}
}

func roleBindingInheritedFrom(ctx context.Context, nsName, roleBindingName string) string {
nnm := types.NamespacedName{Namespace: nsName, Name: roleBindingName}
roleBinding := &v1.RoleBinding{}
if err := k8sClient.Get(ctx, nnm, roleBinding); err != nil {
// should have been caught above
return err.Error()
}
if roleBinding.ObjectMeta.Labels == nil {
return ""
}
lif, _ := roleBinding.ObjectMeta.Labels["hnc.x-k8s.io/inheritedFrom"]
return lif
}

func makeResourceQuota(ctx context.Context, nsName, resourceQuotaName string) {
resourceQuota := &corev1.ResourceQuota{}
resourceQuota.Name = resourceQuotaName
resourceQuota.Namespace = nsName
ExpectWithOffset(1, k8sClient.Create(ctx, resourceQuota)).Should(Succeed())
}

func hasResourceQuota(ctx context.Context, nsName, resourceQuotaName string) bool {
// `Eventually` only works with a fn that doesn't take any args
nnm := types.NamespacedName{Namespace: nsName, Name: resourceQuotaName}
resourceQuota := &corev1.ResourceQuota{}
err := k8sClient.Get(ctx, nnm, resourceQuota)
return err == nil
}
60 changes: 2 additions & 58 deletions incubator/hnc/pkg/reconcilers/object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)

Expand All @@ -28,7 +27,7 @@ var _ = Describe("Secret", func() {
barName = createNS(ctx, "bar")
bazName = createNS(ctx, "baz")

// Give them each a secret
// Give them each a role.
makeRole(ctx, fooName, "foo-role")
makeRole(ctx, barName, "bar-role")
makeRole(ctx, bazName, "baz-role")
Expand Down Expand Up @@ -60,7 +59,7 @@ var _ = Describe("Secret", func() {
// Creates an empty ConfigMap. We use ConfigMap for this test because the apiserver will not
// add additional fields to an empty ConfigMap object to make it non-empty.
makeConfigMap(ctx, fooName, "foo-config")
addConfigMapToHNCConfig(ctx)
addToHNCConfig(ctx, "v1", "ConfigMap", api.Propagate)

// "foo-config" should now be propagated from foo to bar.
Eventually(hasConfigMap(ctx, barName, "foo-config")).Should(BeTrue())
Expand Down Expand Up @@ -243,52 +242,6 @@ func newOrGetHierarchy(ctx context.Context, nm string) *api.HierarchyConfigurati
return hier
}

func makeRole(ctx context.Context, nsName, roleName string) {
role := &v1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: roleName,
Namespace: nsName,
},
TypeMeta: metav1.TypeMeta{
Kind: "Role",
APIVersion: "rbac.authorization.k8s.io/v1",
},
Rules: []v1.PolicyRule{
// Allow the users to read all secrets, namespaces and configmaps.
{
APIGroups: []string{""},
Resources: []string{"secrets", "namespaces", "configmaps"},
Verbs: []string{"get", "watch", "list"},
},
},
}
ExpectWithOffset(1, k8sClient.Create(ctx, role)).Should(Succeed())
}

func hasRole(ctx context.Context, nsName, roleName string) func() bool {
// `Eventually` only works with a fn that doesn't take any args
return func() bool {
nnm := types.NamespacedName{Namespace: nsName, Name: roleName}
role := &v1.Role{}
err := k8sClient.Get(ctx, nnm, role)
return err == nil
}
}

func roleInheritedFrom(ctx context.Context, nsName, roleName string) string {
nnm := types.NamespacedName{Namespace: nsName, Name: roleName}
role := &v1.Role{}
if err := k8sClient.Get(ctx, nnm, role); err != nil {
// should have been caught above
return err.Error()
}
if role.ObjectMeta.Labels == nil {
return ""
}
lif, _ := role.ObjectMeta.Labels["hnc.x-k8s.io/inheritedFrom"]
return lif
}

func modifyRole(ctx context.Context, nsName, roleName string) {
nnm := types.NamespacedName{Namespace: nsName, Name: roleName}
role := &v1.Role{}
Expand Down Expand Up @@ -332,15 +285,6 @@ func removeRole(ctx context.Context, nsName, roleName string) {
ExpectWithOffset(1, k8sClient.Delete(ctx, role)).Should(Succeed())
}

func addConfigMapToHNCConfig(ctx context.Context) {
Eventually(func() error {
c := getHNCConfig(ctx)
configMap := api.TypeSynchronizationSpec{APIVersion: "v1", Kind: "ConfigMap", Mode: api.Propagate}
c.Spec.Types = append(c.Spec.Types, configMap)
return updateHNCConfig(ctx, c)
}).Should(Succeed())
}

// Makes an empty ConfigMap object.
func makeConfigMap(ctx context.Context, nsName, configMapName string) {
configMap := &corev1.ConfigMap{}
Expand Down
Loading