Skip to content

Commit

Permalink
Introduce rosaControlPlane for managed kubernetes (#4453)
Browse files Browse the repository at this point in the history
* Introduce rosaControlPlane for managed kubernetes

* fixed capatilized resource names

* code cleanup

* move rosacluster_types.go to exp/api

* Add AvailabilityZones field to ROSAControlPlane

* implemented deletion

* adressed code review

* adapt code after CAPI bump to v1.5.0

---------

Co-authored-by: enxebre <alberto.garcial@hotmail.com>
  • Loading branch information
muraee and enxebre committed Oct 5, 2023
1 parent 9eb88c7 commit b36e26e
Show file tree
Hide file tree
Showing 16 changed files with 2,361 additions and 1 deletion.
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ generate-go-apis: ## Alias for .build/generate-go-apis
paths=./$(EXP_DIR)/api/... \
paths=./bootstrap/eks/api/... \
paths=./controlplane/eks/api/... \
paths=./controlplane/rosa/api/... \
paths=./iam/api/... \
paths=./controllers/... \
paths=./$(EXP_DIR)/controllers/... \
Expand Down Expand Up @@ -272,6 +273,14 @@ generate-go-apis: ## Alias for .build/generate-go-apis
--output-file-base=zz_generated.conversion $(GEN_OUTPUT_BASE) \
--go-header-file=./hack/boilerplate/boilerplate.generatego.txt

$(CONVERSION_GEN) \
--input-dirs=./controlplane/rosa/api/v1beta2 \
--extra-peer-dirs=sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta1 \
--extra-peer-dirs=sigs.k8s.io/cluster-api/api/v1beta1 \
--build-tag=ignore_autogenerated_conversions \
--output-file-base=zz_generated.conversion $(GEN_OUTPUT_BASE) \
--go-header-file=./hack/boilerplate/boilerplate.generatego.txt

touch $@

##@ lint and verify:
Expand Down Expand Up @@ -374,7 +383,7 @@ managers: ## Alias for manager-aws-infrastructure

.PHONY: manager-aws-infrastructure
manager-aws-infrastructure: ## Build manager binary
CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH} go build -ldflags "${LDFLAGS} -extldflags '-static'" -o $(BIN_DIR)/manager .
CGO_ENABLED=0 GOARCH=${ARCH} go build -ldflags "${LDFLAGS} -extldflags '-static'" -o $(BIN_DIR)/manager .

##@ test:

Expand Down
339 changes: 339 additions & 0 deletions config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml

Large diffs are not rendered by default.

100 changes: 100 additions & 0 deletions config/crd/bases/infrastructure.cluster.x-k8s.io_rosaclusters.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.12.1
name: rosaclusters.infrastructure.cluster.x-k8s.io
spec:
group: infrastructure.cluster.x-k8s.io
names:
categories:
- cluster-api
kind: ROSACluster
listKind: ROSAClusterList
plural: rosaclusters
shortNames:
- rosac
singular: rosacluster
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: Cluster to which this AWSManagedControl belongs
jsonPath: .metadata.labels.cluster\.x-k8s\.io/cluster-name
name: Cluster
type: string
- description: Control plane infrastructure is ready for worker nodes
jsonPath: .status.ready
name: Ready
type: string
- description: API Endpoint
jsonPath: .spec.controlPlaneEndpoint.host
name: Endpoint
priority: 1
type: string
name: v1beta2
schema:
openAPIV3Schema:
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
properties:
controlPlaneEndpoint:
description: ControlPlaneEndpoint represents the endpoint used to
communicate with the control plane.
properties:
host:
description: The hostname on which the API server is serving.
type: string
port:
description: The port on which the API server is serving.
format: int32
type: integer
required:
- host
- port
type: object
type: object
status:
description: ROSAClusterStatus defines the observed state of ROSACluster
properties:
failureDomains:
additionalProperties:
description: FailureDomainSpec is the Schema for Cluster API failure
domains. It allows controllers to understand how many failure
domains a cluster can optionally span across.
properties:
attributes:
additionalProperties:
type: string
description: Attributes is a free form map of attributes an
infrastructure provider might use or require.
type: object
controlPlane:
description: ControlPlane determines if this failure domain
is suitable for use by control plane machines.
type: boolean
type: object
description: FailureDomains specifies a list fo available availability
zones that can be used
type: object
ready:
description: Ready is when the ROSAControlPlane has a API server URL.
type: boolean
type: object
type: object
served: true
storage: true
subresources:
status: {}
28 changes: 28 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ rules:
- get
- patch
- update
- apiGroups:
- controlplane.cluster.x-k8s.io
resources:
- rosacontrolplanes
- rosacontrolplanes/status
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
Expand Down Expand Up @@ -331,3 +340,22 @@ rules:
- get
- patch
- update
- apiGroups:
- infrastructure.cluster.x-k8s.io
resources:
- rosaclusters
verbs:
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- infrastructure.cluster.x-k8s.io
resources:
- rosaclusters/status
verbs:
- get
- patch
- update
201 changes: 201 additions & 0 deletions controllers/rosacluster_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*
Copyright 2023 The Kubernetes Authors.
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"
"fmt"

"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"k8s.io/klog/v2"
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/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"

infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
rosacontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/rosa/api/v1beta2"
expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2"
"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/logger"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/cluster-api/util"
"sigs.k8s.io/cluster-api/util/annotations"
"sigs.k8s.io/cluster-api/util/patch"
"sigs.k8s.io/cluster-api/util/predicates"
)

// ROSAClusterReconciler reconciles ROSACluster.
type ROSAClusterReconciler struct {
client.Client
Recorder record.EventRecorder
WatchFilterValue string
}

// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=rosaclusters,verbs=get;list;watch;update;patch;delete
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=rosaclusters/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=controlplane.cluster.x-k8s.io,resources=rosacontrolplanes;rosacontrolplanes/status,verbs=get;list;watch
// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters;clusters/status,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete

func (r *ROSAClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) {
log := ctrl.LoggerFrom(ctx)
log.Info("Reconciling ROSACluster")

// Fetch the ROSACluster instance
rosaCluster := &expinfrav1.ROSACluster{}
err := r.Get(ctx, req.NamespacedName, rosaCluster)
if err != nil {
if apierrors.IsNotFound(err) {
return reconcile.Result{}, nil
}
return reconcile.Result{}, err
}

// Fetch the Cluster.
cluster, err := util.GetOwnerCluster(ctx, r.Client, rosaCluster.ObjectMeta)
if err != nil {
return reconcile.Result{}, err
}
if cluster == nil {
log.Info("Cluster Controller has not yet set OwnerRef")
return reconcile.Result{}, nil
}

if annotations.IsPaused(cluster, rosaCluster) {
log.Info("ROSACluster or linked Cluster is marked as paused. Won't reconcile")
return reconcile.Result{}, nil
}

log = log.WithValues("cluster", cluster.Name)

controlPlane := &rosacontrolplanev1.ROSAControlPlane{}
controlPlaneRef := types.NamespacedName{
Name: cluster.Spec.ControlPlaneRef.Name,
Namespace: cluster.Spec.ControlPlaneRef.Namespace,
}

if err := r.Get(ctx, controlPlaneRef, controlPlane); err != nil {
return reconcile.Result{}, fmt.Errorf("failed to get control plane ref: %w", err)
}

log = log.WithValues("controlPlane", controlPlaneRef.Name)

patchHelper, err := patch.NewHelper(rosaCluster, r.Client)
if err != nil {
return reconcile.Result{}, fmt.Errorf("failed to init patch helper: %w", err)
}

// Set the values from the managed control plane
rosaCluster.Status.Ready = true
rosaCluster.Spec.ControlPlaneEndpoint = controlPlane.Spec.ControlPlaneEndpoint
// rosaCluster.Status.FailureDomains = controlPlane.Status.FailureDomains

if err := patchHelper.Patch(ctx, rosaCluster); err != nil {
return reconcile.Result{}, fmt.Errorf("failed to patch ROSACluster: %w", err)
}

log.Info("Successfully reconciled ROSACluster")

return reconcile.Result{}, nil
}

func (r *ROSAClusterReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
log := logger.FromContext(ctx)

rosaCluster := &expinfrav1.ROSACluster{}

controller, err := ctrl.NewControllerManagedBy(mgr).
WithOptions(options).
For(rosaCluster).
WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)).
Build(r)

if err != nil {
return fmt.Errorf("error creating controller: %w", err)
}

// Add a watch for clusterv1.Cluster unpaise
if err = controller.Watch(
source.Kind(mgr.GetCache(), &clusterv1.Cluster{}),
handler.EnqueueRequestsFromMapFunc(util.ClusterToInfrastructureMapFunc(ctx, infrav1.GroupVersion.WithKind("ROSACluster"), mgr.GetClient(), &expinfrav1.ROSACluster{})),
predicates.ClusterUnpaused(log.GetLogger()),
); err != nil {
return fmt.Errorf("failed adding a watch for ready clusters: %w", err)
}

// Add a watch for ROSAControlPlane
if err = controller.Watch(
source.Kind(mgr.GetCache(), &rosacontrolplanev1.ROSAControlPlane{}),
handler.EnqueueRequestsFromMapFunc(r.rosaControlPlaneToManagedCluster(log)),
); err != nil {
return fmt.Errorf("failed adding watch on ROSAControlPlane: %w", err)
}

return nil
}

func (r *ROSAClusterReconciler) rosaControlPlaneToManagedCluster(log *logger.Logger) handler.MapFunc {
return func(ctx context.Context, o client.Object) []ctrl.Request {
ROSAControlPlane, ok := o.(*rosacontrolplanev1.ROSAControlPlane)
if !ok {
log.Error(errors.Errorf("expected an ROSAControlPlane, got %T instead", o), "failed to map ROSAControlPlane")
return nil
}

log := log.WithValues("objectMapper", "awsmcpTomc", "ROSAcontrolplane", klog.KRef(ROSAControlPlane.Namespace, ROSAControlPlane.Name))

if !ROSAControlPlane.ObjectMeta.DeletionTimestamp.IsZero() {
log.Info("ROSAControlPlane has a deletion timestamp, skipping mapping")
return nil
}

if ROSAControlPlane.Spec.ControlPlaneEndpoint.IsZero() {
log.Debug("ROSAControlPlane has no control plane endpoint, skipping mapping")
return nil
}

cluster, err := util.GetOwnerCluster(ctx, r.Client, ROSAControlPlane.ObjectMeta)
if err != nil {
log.Error(err, "failed to get owning cluster")
return nil
}
if cluster == nil {
log.Info("no owning cluster, skipping mapping")
return nil
}

managedClusterRef := cluster.Spec.InfrastructureRef
if managedClusterRef == nil || managedClusterRef.Kind != "ROSACluster" {
log.Info("InfrastructureRef is nil or not ROSACluster, skipping mapping")
return nil
}

return []ctrl.Request{
{
NamespacedName: types.NamespacedName{
Name: managedClusterRef.Name,
Namespace: managedClusterRef.Namespace,
},
},
}
}
}

0 comments on commit b36e26e

Please sign in to comment.