Skip to content

Commit

Permalink
Compute NumPropagatedObjects in status
Browse files Browse the repository at this point in the history
This PR computes NumPropagatedObjects as a part of HNCConfiguration
status. NumPropagatedObjects indicates the number of propagated objects
of a specific type created by HNC.

Tested: unit tests, GKE cluster

Issue: kubernetes-retired#411
  • Loading branch information
sophieliu15 committed Mar 25, 2020
1 parent ca59e4a commit d48e242
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 64 deletions.
5 changes: 3 additions & 2 deletions incubator/hnc/api/v1alpha1/hnc_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,11 @@ type TypeSynchronizationStatus struct {
// Kind to be configured.
Kind string `json:"kind,omitempty"`

// Tracks the number of original objects that are being propagated to descendant namespaces.
// Tracks the number of objects that are being propagated to descendant namespaces. The propagated
// objects are created by HNC.
// +kubebuilder:validation:Minimum=0
// +optional
NumPropagated *int32 `json:"numPropagated,omitempty"`
NumPropagatedObjects *int32 `json:"numPropagatedObjects,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
4 changes: 2 additions & 2 deletions incubator/hnc/api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,10 @@ spec:
kind:
description: Kind to be configured.
type: string
numPropagated:
description: Tracks the number of original objects that are being
propagated to descendant namespaces.
numPropagatedObjects:
description: Tracks the number of objects that are being propagated
to descendant namespaces. The propagated objects are created
by HNC.
format: int32
minimum: 0
type: integer
Expand Down
12 changes: 12 additions & 0 deletions incubator/hnc/pkg/forest/forest.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ type TypeSyncer interface {
// SetMode sets the propagation mode of objects that are handled by the reconciler who implements the interface.
// The method also syncs objects in the cluster for the type handled by the reconciler if necessary.
SetMode(context.Context, api.SynchronizationMode, logr.Logger) error
// GetNumPropagatedObjects returns the number of propagated objects on the apiserver.
GetNumPropagatedObjects() int
}

// NumPropagatedObjectsSyncer syncs the number of propagated objects. ConfigReconciler implements the
// interface so that it can be called by an ObjectReconciler if the number of propagated objects is changed.
type NumPropagatedObjectsSyncer interface {
SyncNumPropagatedObjects(logr.Logger)
}

// Forest defines a forest of namespaces - that is, a set of trees. It includes methods to mutate
Expand All @@ -53,6 +61,10 @@ type Forest struct {
// We can also move the lock out of the forest and pass it to all reconcilers that need the lock.
// In that way, we don't need to put the list in the forest.
types []TypeSyncer

// config is the ConfigReconciler that an object reconciler can call if the status of the HNCConfiguration
// object needs to be updated.
Config NumPropagatedObjectsSyncer
}

func NewForest() *Forest {
Expand Down
83 changes: 66 additions & 17 deletions incubator/hnc/pkg/reconcilers/hnc_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package reconcilers
import (
"context"
"fmt"
"sort"
"time"

"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand Down Expand Up @@ -33,22 +35,32 @@ 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
// Trigger 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
// that is used to enqueue the singleton to trigger reconciliation.
Trigger 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.
HierarchyConfigUpdates chan event.GenericEvent

// activeGVKs contains GVKs that are configured in the Spec.
activeGVKs gvkSet
}

// gvkSet keeps track of a group of unique GVKs.
type gvkSet map[schema.GroupVersionKind]bool

// Reconcile sets up some basic variable and logs the Spec.
// TODO: Updates the comment above when adding more logic to the Reconcile method.
func (r *ConfigReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
// Object reconcilers will trigger the config reconciler at the end of each object
// reconciliation for updating the status of the `config` singleton. Sleep here so
// that a batch of reconciliation requests issued by object reconcilers can be
// treated as one request to avoid invoking the config reconciler very frequently.
time.Sleep(3 * time.Second)

ctx := context.Background()

// Validate the singleton name.
Expand All @@ -63,18 +75,17 @@ func (r *ConfigReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
return ctrl.Result{}, err
}

// TODO: Modify this and other reconcilers (e.g., hierarchy and object reconcilers) to
// achieve the reconciliation.
r.Log.Info("Reconciling cluster-wide HNC configuration")

// Clear the existing conditions because we will reconstruct the latest conditions.
// Clear the existing the status because we will reconstruct the latest status.
inst.Status.Conditions = nil
inst.Status.Types = nil

// Create or sync corresponding ObjectReconcilers, if needed.
syncErr := r.syncObjectReconcilers(ctx, inst)

// Add the status for each type.
r.addTypeStatus(inst)

// Write back to the apiserver.
// TODO: Update HNCConfiguration.Status before writing the singleton back to the apiserver.
if err := r.writeSingleton(ctx, inst); err != nil {
r.Log.Error(err, "Couldn't write singleton")
return ctrl.Result{}, err
Expand Down Expand Up @@ -225,13 +236,15 @@ func (r *ConfigReconciler) syncObjectReconcilers(ctx context.Context, inst *api.
func (r *ConfigReconciler) syncActiveReconcilers(ctx context.Context, inst *api.HNCConfiguration) error {
// exist keeps track of existing types in the `config` singleton.
exist := gvkSet{}
r.activeGVKs = gvkSet{}
for _, t := range inst.Spec.Types {
// If there are multiple configurations of the same type, we will follow the first
// configuration and ignore the rest.
if !r.ensureNoDuplicateTypeConfigurations(inst, t, exist) {
continue
}
gvk := schema.FromAPIVersionAndKind(t.APIVersion, t.Kind)
r.activeGVKs[gvk] = true
if ts := r.Forest.GetTypeSyncer(gvk); ts != nil {
if err := ts.SetMode(ctx, t.Mode, r.Log); err != nil {
return err // retry the reconciliation
Expand Down Expand Up @@ -314,6 +327,7 @@ func (r *ConfigReconciler) createObjectReconciler(gvk schema.GroupVersionKind, m
Mode: mode,
Affected: make(chan event.GenericEvent),
AffectedNamespace: r.HierarchyConfigUpdates,
PropagatedObjects: namespacedNameSet{},
}

// TODO: figure out MaxConcurrentReconciles option - https://github.com/kubernetes-sigs/multi-tenancy/issues/291
Expand Down Expand Up @@ -362,21 +376,51 @@ func (r *ConfigReconciler) validateSingletonName(ctx context.Context, nm string)
return fmt.Errorf("Error while validating singleton name: %s", msg)
}

// 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
// addTypeStatus adds Status.Types for types configured in the spec. The Status.Types
// is sorted in alphabetical order based on APIVersion.
func (r *ConfigReconciler) addTypeStatus(inst *api.HNCConfiguration) {
for _, ts := range r.Forest.GetTypeSyncers() {
if r.activeGVKs[ts.GetGVK()] {
r.addNumPropagatedObjects(ts.GetGVK(), ts.GetNumPropagatedObjects(), inst)
}
}
sort.Slice(inst.Status.Types, func(i, j int) bool {
return inst.Status.Types[i].APIVersion < inst.Status.Types[j].APIVersion
})
}

// addNumPropagatedObjects adds the NumPropagatedObjects field for a given GVK in the status.
func (r *ConfigReconciler) addNumPropagatedObjects(gvk schema.GroupVersionKind, num int,
inst *api.HNCConfiguration) {
apiVersion, kind := gvk.ToAPIVersionAndKind()
n := int32(num)
inst.Status.Types = append(inst.Status.Types, api.TypeSynchronizationStatus{
APIVersion: apiVersion,
Kind: kind,
NumPropagatedObjects: &n,
})
}

// enqueueSingleton enqueues the `config` singleton to trigger the reconciliation
// of the singleton for a given reason . 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) {
func (r *ConfigReconciler) enqueueSingleton(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}
r.Trigger <- event.GenericEvent{Meta: inst}
}()
}

// SyncNumPropagatedObjects is called by an object reconciler to trigger reconciliation of
// the 'config' singleton for updating the status.
func (r *ConfigReconciler) SyncNumPropagatedObjects(log logr.Logger) {
r.enqueueSingleton(log, "Sync NumPropagatedObjects in the status")
}

// SetupWithManager builds a controller with the reconciler.
func (r *ConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
// Whenever a CRD is created/updated, we will send a request to reconcile the
Expand All @@ -392,22 +436,27 @@ func (r *ConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
})
err := ctrl.NewControllerManagedBy(mgr).
For(&api.HNCConfiguration{}).
Watches(&source.Channel{Source: r.Igniter}, &handler.EnqueueRequestForObject{}).
Watches(&source.Channel{Source: r.Trigger}, &handler.EnqueueRequestForObject{}).
Watches(&source.Kind{Type: &v1beta1.CustomResourceDefinition{}},
&handler.EnqueueRequestsFromMapFunc{ToRequests: crdMapFn}).
Complete(r)
if err != nil {
return err
}
// Create a default singleton if there is no singleton in the cluster.
// Create a default singleton if there is no singleton in the cluster by forcing
// reconciliation to start.
//
// 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"+
r.enqueueSingleton(r.Log, "Enforce reconciliation to create a default"+
"HNCConfiguration singleton if it does not exist")

// Informs the forest about the config reconciler so that it can be triggered
// by object reconcilers for updating the status.
r.Forest.Config = r
return nil
}
Loading

0 comments on commit d48e242

Please sign in to comment.