Skip to content

Commit

Permalink
Add managedcluster controller (stolostron#21)
Browse files Browse the repository at this point in the history
* Add new unmanaged controller that delays its start until the managedcluster resource exists

* Upgrades controller-runtime to 0.8.1 to resolve a bug that prevents listing unstructured resources, as well as other version bumps.
The version bump includes a number of breaking changes that needed code changes to get working again.

* Update make command to use most recent yq version

* Add managed status to discovered cluster on creation in discoveryconfig controller. Update managed status in discovered clusters on changes to managed clusters in managedcluster controller

* Add tests for managedcluster_controller

* Add integration tests for managedcluster functionality

* Handle error in discoveryconfig controller when managedcluster resource does not exist

* Have managedcluster crd installed before running integration tests

* Remove commented-out code
  • Loading branch information
JakobGray authored Feb 9, 2021
1 parent 24546c4 commit 4cbc304
Show file tree
Hide file tree
Showing 15 changed files with 1,050 additions and 173 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This project manages discovered clusters

- go version v1.13+
- operator-sdk version v1.3.0
- yq v4.4.x
- docker
- quay credentials for https://quay.io/organization/rhibmcollab and https://quay.io/organization/open-cluster-management
- Connection to an existing Kubernetes cluster
Expand Down
12 changes: 12 additions & 0 deletions config/rbac/leader_election_role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,15 @@ rules:
verbs:
- create
- patch
- apiGroups:
- "coordination.k8s.io"
resources:
- leases
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
8 changes: 8 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ metadata:
creationTimestamp: null
name: discovery-role
rules:
- apiGroups:
- cluster.open-cluster-management.io
resources:
- managedclusters
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
Expand Down
7 changes: 2 additions & 5 deletions controllers/discoveredclusterrefresh_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import (
// DiscoveredClusterRefreshReconciler reconciles a DiscoveredClusterRefresh object
type DiscoveredClusterRefreshReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Trigger chan event.GenericEvent
}
Expand All @@ -41,9 +40,8 @@ type DiscoveredClusterRefreshReconciler struct {
// +kubebuilder:rbac:groups=discovery.open-cluster-management.io,resources=discoveryconfigs,verbs=get;list;watch
// +kubebuilder:rbac:groups=discovery.open-cluster-management.io,resources=discoveryconfigs/status,verbs=get;update;patch

func (r *DiscoveredClusterRefreshReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
log := r.Log.WithValues("discoveredclusterrefresh", req.NamespacedName)
func (r *DiscoveredClusterRefreshReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := logr.FromContext(ctx)

refresh := &discoveryv1.DiscoveredClusterRefresh{}
if err := r.Get(ctx, types.NamespacedName{
Expand Down Expand Up @@ -72,7 +70,6 @@ func (r *DiscoveredClusterRefreshReconciler) Reconcile(req ctrl.Request) (ctrl.R

// Trigger reconcile of found DiscoveryConfig
r.Trigger <- event.GenericEvent{
Meta: config.GetObjectMeta(),
Object: config,
}

Expand Down
36 changes: 31 additions & 5 deletions controllers/discoveryconfig_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"github.com/go-logr/logr"
"github.com/pkg/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ref "k8s.io/client-go/tools/reference"
Expand All @@ -41,12 +42,12 @@ import (
"github.com/open-cluster-management/discovery/util/reconciler"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// DiscoveryConfigReconciler reconciles a DiscoveryConfig object
type DiscoveryConfigReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Trigger chan event.GenericEvent
}
Expand All @@ -64,9 +65,8 @@ type CloudRedHatProviderConnection struct {
// +kubebuilder:rbac:groups=discovery.open-cluster-management.io,resources=discoveryconfigs/finalizers,verbs=get;update;patch
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch

func (r *DiscoveryConfigReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
log := r.Log.WithValues("discoveryconfig", req.NamespacedName)
func (r *DiscoveryConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := logr.FromContext(ctx)

// Get discovery config. Die if there is none
config := &discoveryv1.DiscoveryConfig{}
Expand Down Expand Up @@ -131,6 +131,24 @@ func (r *DiscoveryConfigReconciler) Reconcile(req ctrl.Request) (ctrl.Result, er
existing[cluster.Name] = i
}

// List all managed clusters
managedClusters := &unstructured.UnstructuredList{}
managedClusters.SetGroupVersionKind(managedClusterGVK)
if err := r.List(ctx, managedClusters); err != nil {
// Capture case were ManagedClusters resource does not exist
if !apimeta.IsNoMatchError(err) {
return ctrl.Result{}, errors.Wrapf(err, "error listing managed clusters")
}
}

managedClusterIDs := make(map[string]int, len(managedClusters.Items))
for i, mc := range managedClusters.Items {
name := getClusterID(mc)
if name != "" {
managedClusterIDs[getClusterID(mc)] = i
}
}

var createClusters []discoveryv1.DiscoveredCluster
var updateClusters []discoveryv1.DiscoveredCluster
var deleteClusters []discoveryv1.DiscoveredCluster
Expand Down Expand Up @@ -162,6 +180,11 @@ func (r *DiscoveryConfigReconciler) Reconcile(req ctrl.Request) (ctrl.Result, er
CreatorID: "abc123",
}

// Assign managed status
if _, managed := managedClusterIDs[discoveredCluster.Spec.Name]; managed {
setManagedStatus(&discoveredCluster)
}

// Add reference to secret used for authentication
discoveredCluster.Spec.ProviderConnections = nil
secretRef, err := ref.GetReference(r.Scheme, ocmSecret)
Expand Down Expand Up @@ -234,7 +257,7 @@ func (r *DiscoveryConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
return false
},
UpdateFunc: func(e event.UpdateEvent) bool {
return e.MetaNew.GetGeneration() != e.MetaOld.GetGeneration()
return e.ObjectNew.GetGeneration() != e.ObjectOld.GetGeneration()
},
}).
Build(r)
Expand Down Expand Up @@ -281,6 +304,9 @@ func same(c1, c2 discoveryv1.DiscoveredCluster) bool {
if c1i.State != c2i.State {
return false
}
if c1i.IsManagedCluster != c2i.IsManagedCluster {
return false
}
return true
}

Expand Down
215 changes: 215 additions & 0 deletions controllers/managedcluster_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers

import (
"context"
"time"

"github.com/go-logr/logr"
discoveryv1 "github.com/open-cluster-management/discovery/api/v1"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/source"
)

var managedClusterGVK = schema.GroupVersionKind{
Kind: "ManagedCluster",
Group: "cluster.open-cluster-management.io",
Version: "v1",
}

// ManagedClusterReconciler reconciles a ManagedCluster object
type ManagedClusterReconciler struct {
client.Client
Name string
Scheme *runtime.Scheme
Log logr.Logger
}

// +kubebuilder:rbac:groups=cluster.open-cluster-management.io,resources=managedclusters,verbs=get;list;watch

func (r *ManagedClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := logr.FromContext(ctx)

managedClusters := &unstructured.UnstructuredList{}
managedClusters.SetGroupVersionKind(managedClusterGVK)
if err := r.List(ctx, managedClusters); err != nil {
return ctrl.Result{}, errors.Wrapf(err, "error listing managed clusters")
}

discoveredClusters := &discoveryv1.DiscoveredClusterList{}
if err := r.List(ctx, discoveredClusters, client.InNamespace(req.Namespace)); err != nil {
return ctrl.Result{}, errors.Wrapf(err, "error listing discovered clusters")
}

// Update for recently managed clusters
for _, m := range managedClusters.Items {
id := getClusterID(m)
dc := matchingDiscoveredCluster(discoveredClusters, id)
if dc == nil {
// No matching discovered cluster
log.Info("No matching discovered cluster for managed cluster", "managedCluster id", id)
continue
}

if updateRequired := setManagedStatus(dc); updateRequired {
// Update with managed labels
if err := r.Update(ctx, dc); err != nil {
return ctrl.Result{}, errors.Wrapf(err, "error updating discovered cluster `%s`", id)
}
}
}

// Update for recently unmanaged clusters
for _, dc := range discoveredClusters.Items {
if !dc.Spec.IsManagedCluster {
continue
}
if isManagedCluster(dc, managedClusters) {
continue
}

// Discovered cluster is labeled as managed, but does not have a matching managed cluster
unsetManagedStatus(&dc)

// Update with managed labels removed
if err := r.Update(ctx, &dc); err != nil {
return ctrl.Result{}, errors.Wrapf(err, "error updating discovered cluster `%s`", dc.Name)
}
}

return ctrl.Result{}, nil
}

// SetupWithManager ...
func (r *ManagedClusterReconciler) SetupWithManager(mgr ctrl.Manager) (controller.Controller, error) {
managedClusterController, err := controller.NewUnmanaged(r.Name, mgr, controller.Options{
Reconciler: r,
Log: r.Log,
})
if err != nil {
return nil, errors.Wrapf(err, "error creating controller")
}

u := &unstructured.Unstructured{}
u.SetGroupVersionKind(managedClusterGVK)
// Watch for Pod create / update / delete events and call Reconcile
err = managedClusterController.Watch(
&source.Kind{Type: u},
&handler.EnqueueRequestForObject{},
predicate.LabelChangedPredicate{})
if err != nil {
return nil, errors.Wrapf(err, "error watching managedclusters")
}

return managedClusterController, nil
}

func StartManagedClusterController(c controller.Controller, mgr ctrl.Manager, log logr.Logger) {
// Start our controller in a goroutine so that we do not block.
go func() {
// Block until our controller manager is elected leader. We presume our
// entire process will terminate if we lose leadership, so we don't need
// to handle that.
<-mgr.Elected()

for {
_, err := mgr.GetRESTMapper().RESTMapping(managedClusterGVK.GroupKind(), managedClusterGVK.Version)

if err != nil {
// Do not create controller
log.Info("ManagedCluster resource does not exist: Waiting to start controller")
time.Sleep(10 * time.Second)
continue
}

// Start our controller. This will block until the stop channel is
// closed, or the controller returns an error.
if err := c.Start(context.TODO()); err != nil {
log.Error(err, "cannot run ManagedCluster controller")
}
}
}()
}

func getClusterID(managedCluster unstructured.Unstructured) string {
if labels := managedCluster.GetLabels(); labels != nil {
return labels["clusterID"]
}
return ""
}

// matchingDiscoveredCluster returns the discoveredCluster with the provided id or nil if not found
func matchingDiscoveredCluster(discoveredList *discoveryv1.DiscoveredClusterList, id string) *discoveryv1.DiscoveredCluster {
for i, _ := range discoveredList.Items {
if discoveredList.Items[i].Spec.Name == id {
return &discoveredList.Items[i]
}
}
return nil
}

// setManagedStatus returns true if labels were added and false if the labels already exist
func setManagedStatus(dc *discoveryv1.DiscoveredCluster) bool {
updated := false

if dc.Labels == nil || dc.Labels["isManagedCluster"] != "true" {
labels := make(map[string]string)
if dc.Labels != nil {
labels = dc.Labels
}
labels["isManagedCluster"] = "true"
dc.SetLabels(labels)
updated = true
}

if !dc.Spec.IsManagedCluster {
dc.Spec.IsManagedCluster = true
updated = true
}

return updated
}

// unsetManagedStatus returns true if labels were removed and false if the labels aren't present
func unsetManagedStatus(dc *discoveryv1.DiscoveredCluster) bool {
updated := false
if dc.Labels["isManagedCluster"] == "true" {
delete(dc.Labels, "isManagedCluster")
updated = true
}
if dc.Spec.IsManagedCluster == true {
dc.Spec.IsManagedCluster = false
updated = true
}
return updated
}

func isManagedCluster(dc discoveryv1.DiscoveredCluster, managedClusters *unstructured.UnstructuredList) bool {
discoveredName := dc.Spec.Name
for _, mc := range managedClusters.Items {
id := getClusterID(mc)
if id != "" && id == discoveredName {
return true
}
}
return false
}
Loading

0 comments on commit 4cbc304

Please sign in to comment.