Skip to content

Commit

Permalink
cluster placement reconciler logic (Azure#172)
Browse files Browse the repository at this point in the history
Co-authored-by: Ryan Zhang <[email protected]>
  • Loading branch information
ryanzhang-oss and Ryan Zhang authored Jul 14, 2022
1 parent 087fc50 commit 6d6c860
Show file tree
Hide file tree
Showing 7 changed files with 452 additions and 4 deletions.
41 changes: 39 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,49 @@ require (
github.com/stretchr/testify v1.7.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
k8s.io/apimachinery v0.23.5
k8s.io/apiserver v0.23.0
k8s.io/client-go v0.23.5
k8s.io/klog v1.0.0
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b
sigs.k8s.io/controller-runtime v0.11.0
)

require (
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/emicklei/go-restful v2.9.5+incompatible // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
go.etcd.io/etcd/api/v3 v3.5.1 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.1 // indirect
go.etcd.io/etcd/client/v3 v3.5.0 // indirect
go.opentelemetry.io/contrib v0.20.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0 // indirect
go.opentelemetry.io/otel v0.20.0 // indirect
go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect
go.opentelemetry.io/otel/metric v0.20.0 // indirect
go.opentelemetry.io/otel/sdk v0.20.0 // indirect
go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect
go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect
go.opentelemetry.io/otel/trace v0.20.0 // indirect
go.opentelemetry.io/proto/otlp v0.7.0 // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
google.golang.org/grpc v1.42.0 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.25 // indirect
)

require (
github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect
Expand Down Expand Up @@ -60,7 +97,7 @@ require (
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
Expand All @@ -70,7 +107,7 @@ require (
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/api v0.23.5
k8s.io/apiextensions-apiserver v0.23.5 // indirect
k8s.io/component-base v0.23.5 // indirect
k8s.io/component-base v0.23.5
k8s.io/klog/v2 v2.60.1
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
Expand Down
53 changes: 53 additions & 0 deletions go.sum

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions pkg/controllers/clusterresourceplacement/cluster_selector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package clusterresourceplacement

import (
"fmt"

"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"

fleetv1alpha1 "go.goms.io/fleet/apis/v1alpha1"
"go.goms.io/fleet/pkg/utils"
)

// selectClusters selected the resources according to the placement resourceSelectors and
// update the results in its status
func (r *Reconciler) selectClusters(placement *fleetv1alpha1.ClusterResourcePlacement) ([]string, error) {
if len(placement.Spec.Policy.ClusterNames) != 0 {
klog.V(4).InfoS("use the cluster names provided as the list of cluster we select", "placement", placement.Name)
return placement.Spec.Policy.ClusterNames, nil
}
selectedClusters := make(map[string]bool)
for _, clusterSelector := range placement.Spec.Policy.Affinity.ClusterAffinity.ClusterSelectorTerms {
clusterNames, err := r.listClusters(&clusterSelector.LabelSelector)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("selector = %v", clusterSelector.LabelSelector))
}
for _, clusterName := range clusterNames {
selectedClusters[clusterName] = true
}
}
clusterNames := make([]string, 0)
for cluster := range selectedClusters {
klog.V(5).InfoS("matched a cluster", "cluster", cluster, "placement", placement.Name)
clusterNames = append(clusterNames, cluster)
}
return clusterNames, nil
}

// listClusters retrieves the clusters according to its label selector, this will hit the informer cache.
func (r *Reconciler) listClusters(labelSelector *metav1.LabelSelector) ([]string, error) {
if !r.memberClusterInformerSynced && !r.InformerManager.IsInformerSynced(utils.MemberClusterGVR) {
return nil, fmt.Errorf("informer cache for memberCluster is not synced yet")
}
r.memberClusterInformerSynced = true

clusterSelector, err := metav1.LabelSelectorAsSelector(labelSelector)
if err != nil {
return nil, errors.Wrap(err, "cannot convert the label clusterSelector to a clusterSelector")
}
clusterNames := make([]string, 0)
objs, err := r.InformerManager.Lister(utils.MemberClusterGVR).List(clusterSelector)
if err != nil {
return nil, errors.Wrap(err, "failed to list the clusters according to obj label selector")
}
for _, obj := range objs {
clusterObj, err := meta.Accessor(obj)
if err != nil {
return nil, errors.Wrap(err, "cannot get the name of a cluster object")
}
clusterNames = append(clusterNames, clusterObj.GetName())
}
return clusterNames, nil
}
129 changes: 129 additions & 0 deletions pkg/controllers/clusterresourceplacement/placement_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the MIT license.
*/

package clusterresourceplacement

import (
"context"
"fmt"
"time"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/client-go/tools/record"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

fleetv1alpha1 "go.goms.io/fleet/apis/v1alpha1"
"go.goms.io/fleet/pkg/utils"
"go.goms.io/fleet/pkg/utils/controller"
)

const (
eventReasonResourceSelected = "ResourceSelected"
)

// Reconciler reconciles a cluster resource placement object
type Reconciler struct {
// the informer contains the cache for all the resource we need
InformerManager utils.InformerManager
RestMapper meta.RESTMapper

// DisabledResourceConfig contains all the api resources that we won't select
DisabledResourceConfig *utils.DisabledResourceConfig

Recorder record.EventRecorder

// optimization flag to not have to check the cache sync all the time
placementInformerSynced bool
memberClusterInformerSynced bool
}

func (r *Reconciler) Reconcile(ctx context.Context, key controller.QueueKey) (ctrl.Result, error) {
name, ok := key.(string)
if !ok {
err := fmt.Errorf("get place key %+v not of type string", key)
klog.ErrorS(err, "we have encountered a fatal error that can't be retried, requeue after a day")
return ctrl.Result{RequeueAfter: time.Hour * 24}, nil
}

placement, err := r.getPlacement(name)
if err != nil {
if !apierrors.IsNotFound(err) {
klog.ErrorS(err, "failed to get the cluster resource placement in hub agent", "placement", name)
}
return ctrl.Result{}, client.IgnoreNotFound(err)
}

//TODO: handle finalizer/GC
placementCopy := placement.DeepCopy()
klog.V(2).InfoS("Start to reconcile a ClusterResourcePlacement", "placement", name)
if err = r.createWorkResources(placementCopy); err != nil {
klog.ErrorS(err, "failed to create the work resource for this placement", "placement", placement)
return ctrl.Result{}, err
}

_, err = r.selectClusters(placementCopy)
if err != nil {
klog.ErrorS(err, "failed to select the clusters ", "placement", placement)
return ctrl.Result{}, err
}

// TODO: place works for each cluster and place them in the cluster scoped namespace

// Update the status of the placement
//err = r.InformerManager.GetClient().Resource(utils.ClusterResourcePlacementGVR).Update(ctx, placementCopy, metav1.UpdateOptions{})

return ctrl.Result{}, err
}

// getPlacement retrieves a ClusterResourcePlacement object by its name, this will hit the informer cache.
func (r *Reconciler) getPlacement(name string) (*fleetv1alpha1.ClusterResourcePlacement, error) {
if !r.placementInformerSynced && !r.InformerManager.IsInformerSynced(utils.ClusterResourcePlacementGVR) {
return nil, fmt.Errorf("informer cache for ClusterResourcePlacement is not synced yet")
}
r.placementInformerSynced = true
obj, err := r.InformerManager.Lister(utils.ClusterResourcePlacementGVR).Get(name)
if err != nil {
return nil, err
}
return obj.(*fleetv1alpha1.ClusterResourcePlacement), nil
}

// createWorkResources selects the resources according to the placement resourceSelectors,
// creates a work obj for the resources and updates the results in its status.
func (r *Reconciler) createWorkResources(placement *fleetv1alpha1.ClusterResourcePlacement) error {
objects, err := r.gatherSelectedResource(placement)
if err != nil {
return err
}
klog.V(2).InfoS("Successfully gathered all selected resources", "placement", placement.Name, "number of resources", len(objects))
placement.Status.SelectedResources = make([]fleetv1alpha1.ResourceIdentifier, 0)
for _, obj := range objects {
metaObj, err := meta.Accessor(obj)
if err != nil {
// not sure if we can ever get here, just skip this resource
klog.Warningf("cannot get the name of a runtime object %+v with err %v", obj, err)
continue
}
gvk := obj.GetObjectKind().GroupVersionKind()
res := fleetv1alpha1.ResourceIdentifier{
Group: gvk.Group,
Version: gvk.Version,
Kind: gvk.Kind,
Name: metaObj.GetName(),
Namespace: metaObj.GetNamespace(),
}
placement.Status.SelectedResources = append(placement.Status.SelectedResources, res)
klog.V(5).InfoS("selected one resource ", "placement", placement.Name, "resource", res)
}
r.Recorder.Event(placement, corev1.EventTypeNormal, eventReasonResourceSelected, "successfully gathered all selected resources")

// TODO: Create works objects

return nil
}
Loading

0 comments on commit 6d6c860

Please sign in to comment.