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 26, 2020
1 parent ca59e4a commit 1591a2d
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 58 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 *int `json:"numPropagatedObjects,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
6 changes: 3 additions & 3 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,10 +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.
format: int32
numPropagatedObjects:
description: Tracks the number of objects that are being propagated
to descendant namespaces. The propagated objects are created
by HNC.
minimum: 0
type: integer
type: object
Expand Down
14 changes: 14 additions & 0 deletions incubator/hnc/pkg/forest/forest.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ 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
// GetMode gets the propagation mode of objects that are handled by the reconciler who implements the interface.
GetMode() api.SynchronizationMode
// 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 +63,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

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

func NewForest() *Forest {
Expand Down
114 changes: 96 additions & 18 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,19 +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

// shouldReconcile is used by object reconcilers to signal config reconciler to reconcile.
// Object reconcilers will set shouldReconcile to be true when an object is successfully reconciled
// and the status of the `config` singleton might need to be updated.
shouldReconcile bool
}

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

// checkPeriod is the period that the config reconciler checks if it needs to reconcile the
// `config` singleton.
const checkPeriod = 3 * time.Second

// 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) {
Expand All @@ -63,18 +78,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 +239,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 @@ -311,9 +327,10 @@ func (r *ConfigReconciler) createObjectReconciler(gvk schema.GroupVersionKind, m
Log: ctrl.Log.WithName("reconcilers").WithName(gvk.Kind),
Forest: r.Forest,
GVK: gvk,
Mode: mode,
Mode: GetValidateMode(mode, r.Log),
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 +379,70 @@ 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. Only the status of
// types in `propagate` and `remove` modes will be recorded. 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()] && ts.GetMode() != api.Ignore {
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()
inst.Status.Types = append(inst.Status.Types, api.TypeSynchronizationStatus{
APIVersion: apiVersion,
Kind: kind,
NumPropagatedObjects: &num,
})
}

// 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}
}()
}

// periodicTrigger periodically checks if the `config` singleton needs to be reconciled and
// enqueues the `config` singleton for reconciliation, if needed.
// Object reconcilers signal the config reconciler to reconcile when the status needs to
// be updated. The config reconciler only reconciles periodically so that the reconciliation
// won't be triggered too frequently.
func (r *ConfigReconciler) periodicTrigger() {
// run forever
for {
time.Sleep(checkPeriod)
if r.shouldReconcile == false {
continue
}
r.enqueueSingleton(r.Log, "Syncing NumPropagatedObjects in the status")
r.shouldReconcile = false
}
}

// SyncNumPropagatedObjects will be called by object reconcilers to signal config
// reconciler to reconcile when an object is reconciled successfully and the status of
// the `config` object might need to be updated.
func (r *ConfigReconciler) SyncNumPropagatedObjects(log logr.Logger) {
log.V(1).Info("Signalling config reconciler for reconciliation.")
r.shouldReconcile = true
}

// 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 +458,34 @@ 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")

// Inform the forest about the config reconciler so that object reconcilers can
// signal the config reconciler to reconcile.
r.Forest.ObjectsStatusSyncer = r

// Periodically checks if the config reconciler needs to reconcile and trigger the
// reconciliation if needed, in case the status needs to be updated. This occurs
// in a goroutine so the caller doesn't block; since the reconciler is never
// garbage-collected, this is safe.
go r.periodicTrigger()

return nil
}
Loading

0 comments on commit 1591a2d

Please sign in to comment.