Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/v1alpha1/metalstackcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ const (
// ClusterFinalizer allows to clean up resources associated with before removing it from the apiserver.
ClusterFinalizer = "metal-stack.infrastructure.cluster.x-k8s.io/cluster"

TagInfraClusterResource = "metal-stack.infrastructure.cluster.x-k8s.io/cluster-resource"

ClusterControlPlaneEndpointDefaultPort = 443

ClusterControlPlaneIPEnsured clusterv1.ConditionType = "ClusterControlPlaneIPEnsured"
ClusterPaused clusterv1.ConditionType = clusterv1.PausedV1Beta2Condition
)

var (
Expand Down
3 changes: 2 additions & 1 deletion api/v1alpha1/metalstackmachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ const (
// MachineFinalizer allows to clean up resources associated with before removing it from the apiserver.
MachineFinalizer = "metal-stack.infrastructure.cluster.x-k8s.io/machine"

TagInfraMachineID = "metal-stack.infrastructure.cluster.x-k8s.io/machine-id"
TagInfraMachineResource = "metal-stack.infrastructure.cluster.x-k8s.io/machine-resource"

ProviderMachineCreated clusterv1.ConditionType = "MachineCreated"
ProviderMachineReady clusterv1.ConditionType = "MachineReady"
ProviderMachineHealthy clusterv1.ConditionType = "MachineHealthy"
ProviderMachinePaused clusterv1.ConditionType = clusterv1.PausedV1Beta2Condition
)

// MetalStackMachineSpec defines the desired state of MetalStackMachine.
Expand Down
73 changes: 27 additions & 46 deletions internal/controller/metalstackcluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"errors"
"fmt"
"net/http"

ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -31,6 +32,7 @@ import (

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/conditions"
"sigs.k8s.io/cluster-api/util/patch"
"sigs.k8s.io/cluster-api/util/predicates"
Expand All @@ -46,11 +48,6 @@ import (
metalgo "github.com/metal-stack/metal-go"
)

var (
errProviderIPNotFound = errors.New("provider ip not found")
errProviderIPTooManyFound = errors.New("multiple provider ips found")
)

// MetalStackClusterReconciler reconciles a MetalStackCluster object
type MetalStackClusterReconciler struct {
MetalClient metalgo.Client
Expand Down Expand Up @@ -111,12 +108,21 @@ func (r *MetalStackClusterReconciler) Reconcile(ctx context.Context, req ctrl.Re
return ctrl.Result{}, err
}

if !infraCluster.DeletionTimestamp.IsZero() {
if annotations.IsPaused(cluster, infraCluster) {
conditions.MarkTrue(infraCluster, v1alpha1.ClusterPaused)
} else {
conditions.MarkFalse(infraCluster, v1alpha1.ClusterPaused, clusterv1.PausedV1Beta2Reason, clusterv1.ConditionSeverityInfo, "")
}

switch {
case annotations.IsPaused(cluster, infraCluster):
log.Info("reconciliation is paused")
case !infraCluster.DeletionTimestamp.IsZero():
err = reconciler.delete()
} else if !controllerutil.ContainsFinalizer(infraCluster, v1alpha1.ClusterFinalizer) {
case !controllerutil.ContainsFinalizer(infraCluster, v1alpha1.ClusterFinalizer):
log.Info("adding finalizer")
controllerutil.AddFinalizer(infraCluster, v1alpha1.ClusterFinalizer)
} else {
default:
log.Info("reconciling cluster")
err = reconciler.reconcile()
}
Expand Down Expand Up @@ -226,13 +232,22 @@ func (r *clusterReconciler) ensureControlPlaneIP() (string, error) {
}

func (r *clusterReconciler) deleteControlPlaneIP() error {
ip, err := r.findControlPlaneIP()
if err != nil && errors.Is(err, errProviderIPNotFound) {
if r.infraCluster.Spec.ControlPlaneIP == nil {
return nil
}

resp, err := r.metalClient.IP().FindIP(ipmodels.NewFindIPParams().WithID(*r.infraCluster.Spec.ControlPlaneIP).WithContext(r.ctx), nil)
if err != nil {
return fmt.Errorf("unable to delete control plane ip: %w", err)
var r *ipmodels.FindIPDefault
if errors.As(err, &r) && r.Code() == http.StatusNotFound {
return nil
}

return err
}

ip := resp.Payload

if ip.Type != nil && *ip.Type == models.V1IPBaseTypeStatic {
r.log.Info("skip deletion of static control plane ip")
return nil
Expand All @@ -242,46 +257,12 @@ func (r *clusterReconciler) deleteControlPlaneIP() error {
return fmt.Errorf("control plane ip address not set")
}

if ip.Type != nil && *ip.Type == models.V1IPAllocateRequestTypeStatic {
r.log.Info("skipping deletion of static control plane ip", "ip", *ip.Ipaddress)
return nil
}
_, err = r.metalClient.IP().FreeIP(ipmodels.NewFreeIPParams().WithID(*ip.Ipaddress).WithContext(r.ctx), nil)
if err != nil {
return err
}

r.log.Info("deleted control plane ip", "address", *ip.Ipaddress)

return nil
}

func (r *clusterReconciler) findControlPlaneIP() (*models.V1IPResponse, error) {
if r.infraCluster.Spec.ControlPlaneIP != nil {
resp, err := r.metalClient.IP().FindIP(ipmodels.NewFindIPParams().WithID(*r.infraCluster.Spec.ControlPlaneIP).WithContext(r.ctx), nil)
if err != nil {
return nil, err
}

return resp.Payload, nil
}

resp, err := r.metalClient.IP().FindIPs(ipmodels.NewFindIPsParams().WithBody(&models.V1IPFindRequest{
Projectid: r.infraCluster.Spec.ProjectID,
Tags: []string{
tag.New(tag.ClusterID, string(r.infraCluster.GetUID())),
v1alpha1.TagControlPlanePurpose,
},
}).WithContext(r.ctx), nil)
if err != nil {
return nil, err
}

switch len(resp.Payload) {
case 0:
return nil, errProviderIPNotFound
case 1:
return resp.Payload[0], nil
default:
return nil, errProviderIPTooManyFound
}
}
139 changes: 139 additions & 0 deletions internal/controller/metalstackcluster_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,128 @@ var _ = Describe("MetalStackCluster Controller", func() {
})
})

Context("when reconcile is paused", func() {
BeforeEach(func() {
resource.Spec = v1alpha1.MetalStackClusterSpec{
ControlPlaneEndpoint: v1alpha1.APIEndpoint{},
ProjectID: "test-project",
NodeNetworkID: "node-network-id",
ControlPlaneIP: nil,
Partition: "test-partition",
}
})

It("should skip reconciles due to cluster.spec.ready", func() {
Expect(k8sClient.Create(ctx, resource)).To(Succeed())

By("creating the cluster resource and setting the owner reference")
owner := &clusterv1beta1.Cluster{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "owner-",
Namespace: "default",
},
Spec: clusterv1beta1.ClusterSpec{
Paused: true,
},
}
Expect(k8sClient.Create(ctx, owner)).To(Succeed())

resource.OwnerReferences = []metav1.OwnerReference{
*metav1.NewControllerRef(owner, clusterv1beta1.GroupVersion.WithKind("Cluster")),
}
Expect(k8sClient.Update(ctx, resource)).To(Succeed())

typeNamespacedName := types.NamespacedName{
Name: resource.Name,
Namespace: "default",
}
const firstGen = int64(1)

_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())

Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed())
Expect(resource.Generation).To(Equal(firstGen))

Expect(resource.Status.Conditions).To(ContainElement(MatchFields(IgnoreExtras, Fields{
"Type": Equal(v1alpha1.ClusterPaused),
"Status": Equal(corev1.ConditionTrue),
})))

By("idempotence", func() {
_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())

Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed())
Expect(resource.Generation).To(Equal(firstGen))

Expect(resource.Status.Conditions).To(ContainElement(MatchFields(IgnoreExtras, Fields{
"Type": Equal(v1alpha1.ClusterPaused),
"Status": Equal(corev1.ConditionTrue),
})))
})
})

It("should skip reconciles due to infra annotation", func() {
resource.Annotations = map[string]string{
clusterv1beta1.PausedAnnotation: "true",
}
Expect(k8sClient.Create(ctx, resource)).To(Succeed())

By("creating the cluster resource and setting the owner reference")
owner := &clusterv1beta1.Cluster{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "owner-",
Namespace: "default",
},
}
Expect(k8sClient.Create(ctx, owner)).To(Succeed())

resource.OwnerReferences = []metav1.OwnerReference{
*metav1.NewControllerRef(owner, clusterv1beta1.GroupVersion.WithKind("Cluster")),
}
Expect(k8sClient.Update(ctx, resource)).To(Succeed())

typeNamespacedName := types.NamespacedName{
Name: resource.Name,
Namespace: "default",
}
const firstGen = int64(1)

_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())

Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed())
Expect(resource.Generation).To(Equal(firstGen))

Expect(resource.Status.Conditions).To(ContainElement(MatchFields(IgnoreExtras, Fields{
"Type": Equal(v1alpha1.ClusterPaused),
"Status": Equal(corev1.ConditionTrue),
})))

By("idempotence", func() {
_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())

Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed())
Expect(resource.Generation).To(Equal(firstGen))

Expect(resource.Status.Conditions).To(ContainElement(MatchFields(IgnoreExtras, Fields{
"Type": Equal(v1alpha1.ClusterPaused),
"Status": Equal(corev1.ConditionTrue),
})))
})
})
})

Context("reconciliation with auto-acquiring dependent resources", func() {
BeforeEach(func() {
resource.Spec = v1alpha1.MetalStackClusterSpec{
Expand All @@ -113,6 +235,14 @@ var _ = Describe("MetalStackCluster Controller", func() {
}
})

AfterEach(func() {
Expect(resource.Status.Conditions).To(ContainElement(MatchFields(IgnoreExtras, Fields{
"Type": Equal(v1alpha1.ClusterPaused),
"Status": Equal(corev1.ConditionFalse),
"Reason": Equal(clusterv1beta1.PausedV1Beta2Reason),
})))
})

It("should successfully reconcile", func() {
Expect(k8sClient.Create(ctx, resource)).To(Succeed())

Expand Down Expand Up @@ -206,6 +336,7 @@ var _ = Describe("MetalStackCluster Controller", func() {
}))
})
})

Context("reconciliation when external resources are provided", func() {
var (
nodeNetworkID string
Expand All @@ -226,6 +357,14 @@ var _ = Describe("MetalStackCluster Controller", func() {
}
})

AfterEach(func() {
Expect(resource.Status.Conditions).To(ContainElement(MatchFields(IgnoreExtras, Fields{
"Type": Equal(v1alpha1.ClusterPaused),
"Status": Equal(corev1.ConditionFalse),
"Reason": Equal(clusterv1beta1.PausedV1Beta2Reason),
})))
})

When("creating a resource and setting an ownership", func() {
It("should successfully reconcile", func() {
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
Expand Down
Loading
Loading