Skip to content

Commit

Permalink
Use IPAddressClaims
Browse files Browse the repository at this point in the history
  • Loading branch information
Mattes83 committed Jan 30, 2024
1 parent 64ed187 commit 76d5080
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 1 deletion.
16 changes: 16 additions & 0 deletions api/v1alpha1/ionoscloudmachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package v1alpha1

import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
)
Expand All @@ -28,6 +29,13 @@ const (
// ServerCreationFailedReason (Severity=Error) documents a controller detecting
// issues with the creation of the Server.
ServerCreationFailedReason = "ServerCreationFailed"

// IPAddressClaimCreatedCondition documents the creation of the IP Address
IPAddressClaimCreatedCondition clusterv1.ConditionType = "IPAddressClaimCreated"

// IPAddressClaimCreationFailedReason (Severity=Error) documents a controller detecting
// issues with the creation of the IP address.
IPAddressClaimCreationFailedReason = "IPAddressClaimCreationFailed"
)

// IONOSCloudMachineSpec defines the desired state of IONOSCloudMachine
Expand Down Expand Up @@ -70,9 +78,17 @@ type IONOSVolumeSpec struct {
SSHKeys *[]string `json:"sshKeys,omitempty"`
}

// +kubebuilder:validation:XValidation:rule="(has(self.useDHCP) && !has(self.primaryAddressesFrom)) || (!has(self.useDHCP) && has(self.primaryAddressesFrom))", message="You need to specify either useDHCP or primaryAddressesFrom"
type IONOSNicSpec struct {
LanRef IONOSLanRefSpec `json:"lanRef"`
PrimaryIP *string `json:"primaryIP,omitempty"`
// PrimaryAddressesFrom is an IPAddressPools that should be assigned
// to an IPAddressClaims.
// +optional
PrimaryAddressesFrom *corev1.TypedLocalObjectReference `json:"primaryAddressesFrom,omitempty"`
// +kubebuilder:default:=true
// +kubebuilder:validation:Optional
UseDHCP bool `json:"useDHCP"`
//NameTemplate string `json:"nameTemplate"`
}

Expand Down
6 changes: 6 additions & 0 deletions 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 @@ -113,11 +113,39 @@ spec:
required:
- name
type: object
primaryAddressesFrom:
description: PrimaryAddressesFrom is an IPAddressPools that
should be assigned to an IPAddressClaims.
properties:
apiGroup:
description: APIGroup is the group for the resource being
referenced. If APIGroup is not specified, the specified
Kind must be in the core API group. For any other third-party
types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
primaryIP:
type: string
useDHCP:
default: true
type: boolean
required:
- lanRef
type: object
x-kubernetes-validations:
- message: You need to specify either useDHCP or primaryAddressesFrom
rule: (has(self.useDHCP) && !has(self.primaryAddressesFrom)) ||
(!has(self.useDHCP) && has(self.primaryAddressesFrom))
type: array
providerID:
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,41 @@ spec:
required:
- name
type: object
primaryAddressesFrom:
description: PrimaryAddressesFrom is an IPAddressPools
that should be assigned to an IPAddressClaims.
properties:
apiGroup:
description: APIGroup is the group for the resource
being referenced. If APIGroup is not specified,
the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being
referenced
type: string
name:
description: Name is the name of resource being
referenced
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
primaryIP:
type: string
useDHCP:
default: true
type: boolean
required:
- lanRef
type: object
x-kubernetes-validations:
- message: You need to specify either useDHCP or primaryAddressesFrom
rule: (has(self.useDHCP) && !has(self.primaryAddressesFrom))
|| (!has(self.useDHCP) && has(self.primaryAddressesFrom))
type: array
providerID:
type: string
Expand Down
163 changes: 162 additions & 1 deletion internal/controller/ionoscloudmachine_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import (
goctx "context"
b64 "encoding/base64"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"net/http"
"net/netip"
"strings"
"time"

Expand All @@ -39,6 +41,7 @@ import (
apitypes "k8s.io/apimachinery/pkg/types"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1alpha1"
clusterutilv1 "sigs.k8s.io/cluster-api/util"
"sigs.k8s.io/cluster-api/util/annotations"
"sigs.k8s.io/cluster-api/util/conditions"
Expand Down Expand Up @@ -276,6 +279,20 @@ func (r *IONOSCloudMachineReconciler) reconcileNormal(ctx *context.MachineContex
// If the IONOSCloudMachine doesn't have our finalizer, add it.
ctrlutil.AddFinalizer(ctx.IONOSCloudMachine, v1alpha1.MachineFinalizer)

if result, err := r.reconcileIPAddressClaims(ctx); err != nil {
conditions.MarkFalse(ctx.IONOSCloudMachine, v1alpha1.IPAddressClaimCreatedCondition, v1alpha1.IPAddressClaimCreationFailedReason, clusterv1.ConditionSeverityError, err.Error())
return *result, errors.Wrap(err, "failed reconcileIPAddressClaims")
} else if result != nil {
return *result, nil
}

if result, err := r.reconcileIPAddresses(ctx); err != nil {
conditions.MarkFalse(ctx.IONOSCloudMachine, v1alpha1.IPAddressCreatedCondition, v1alpha1.IPAddressCreationFailedReason, clusterv1.ConditionSeverityError, err.Error())
return *result, errors.Wrap(err, "failed reconcileIPAddresses")
} else if result != nil {
return *result, nil
}

if result, err := r.reconcileServer(ctx); err != nil {
conditions.MarkFalse(ctx.IONOSCloudMachine, v1alpha1.ServerCreatedCondition, v1alpha1.ServerCreationFailedReason, clusterv1.ConditionSeverityError, err.Error())
return *result, errors.Wrap(err, "failed reconcileServer")
Expand Down Expand Up @@ -343,8 +360,147 @@ func (r *IONOSCloudMachineReconciler) getBootstrapData(ctx *context.MachineConte
return userdata, nil
}

// createIPAddressClaim sets up the ipam IPAddressClaim object and creates it in
// the API.
func createIPAddressClaim(ctx *context.MachineContext, ipAddrClaimName string, poolRef *corev1.TypedLocalObjectReference) error {
ctx.Logger.Info("creating IPAddressClaim", "name", ipAddrClaimName)
claim := &ipamv1.IPAddressClaim{
ObjectMeta: metav1.ObjectMeta{
Name: ipAddrClaimName,
Namespace: ctx.IONOSCloudMachine.Namespace,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: ctx.IONOSCloudMachine.APIVersion,
Kind: ctx.IONOSCloudMachine.Kind,
Name: ctx.IONOSCloudMachine.Name,
UID: ctx.IONOSCloudMachine.UID,
},
},
Finalizers: []string{v1alpha1.MachineFinalizer},
},
Spec: ipamv1.IPAddressClaimSpec{PoolRef: *poolRef},
}
return ctx.K8sClient.Create(ctx, claim)
}

// IPAddressClaimName returns a name given a VsphereVM name, deviceIndex, and
// poolIndex.
func IPAddressClaimName(vmName string, deviceIndex, poolIndex int) string {
return fmt.Sprintf("%s-%d-%d", vmName, deviceIndex, poolIndex)
}

// reconcileIPAddressClaims ensures that IONOSCloudMachines that are configured with
// .spec.nics.PrimaryAddressesFrom have corresponding IPAddressClaims.
func (r *IONOSCloudMachineReconciler) reconcileIPAddressClaims(ctx *context.MachineContext) (*reconcile.Result, error) {
for devIdx, device := range ctx.IONOSCloudMachine.Spec.Nics {
if device.PrimaryAddressesFrom == nil {
continue
}
// check if claim exists
ipAddrClaim := &ipamv1.IPAddressClaim{}
ipAddrClaimName := IPAddressClaimName(ctx.IONOSCloudMachine.Name, devIdx, 0)
ipAddrClaimKey := apitypes.NamespacedName{
Namespace: ctx.IONOSCloudMachine.Namespace,
Name: ipAddrClaimName,
}
var err error
if err = ctx.K8sClient.Get(ctx, ipAddrClaimKey, ipAddrClaim); err != nil && !apierrors.IsNotFound(err) {
return &reconcile.Result{}, err
}
if err == nil {
ctx.Logger.V(5).Info("IPAddressClaim found", "name", ipAddrClaimName)
}
if apierrors.IsNotFound(err) {
if err = createIPAddressClaim(ctx, ipAddrClaimName, device.PrimaryAddressesFrom); err != nil {
return &reconcile.Result{}, err
}
}
}
return nil, nil
}

// reconcileIPAddresses prevents successful reconciliation of a IONOSCloudMachine
// until an IPAM Provider updates each IPAddressClaim associated to the
// IONOSCloudMachine with a reference to an IPAddress. This function is a no-op if the
// IONOSCloudMachine has no associated IPAddressClaims.
func (r *IONOSCloudMachineReconciler) reconcileIPAddresses(ctx *context.MachineContext) (*reconcile.Result, error) {
for devIdx, device := range ctx.IONOSCloudMachine.Spec.Nics {
if device.PrimaryAddressesFrom == nil {
continue
}

// check if claim exists
ipAddrClaim := &ipamv1.IPAddressClaim{}
ipAddrClaimName := IPAddressClaimName(ctx.IONOSCloudMachine.Name, devIdx, 0)
ipAddrClaimKey := apitypes.NamespacedName{
Namespace: ctx.IONOSCloudMachine.Namespace,
Name: ipAddrClaimName,
}
var err error
ctx.Logger.Info("fetching IPAddressClaim", "name", ipAddrClaimKey.String())
if err = ctx.K8sClient.Get(ctx, ipAddrClaimKey, ipAddrClaim); err != nil && !apierrors.IsNotFound(err) {
ctx.Logger.Error(err, "error fetching IPAddressClaim", "name", ipAddrClaimName)
return &reconcile.Result{}, err
}

ipAddrName := ipAddrClaim.Status.AddressRef.Name
ctx.Logger.Info("fetched IPAddressClaim", "name", ipAddrClaimName, "IPAddressClaim.Status.AddressRef.Name", ipAddrName)
if ipAddrName == "" {
ctx.Logger.Info("IPAddress name was empty on IPAddressClaim", "name", ipAddrClaimName, "IPAddressClaim.Status.AddressRef.Name", ipAddrName)
msg := "Waiting for IPAddressClaim to have an IPAddress bound"
return &reconcile.Result{}, errors.New(msg)
}

ipAddr := &ipamv1.IPAddress{}
ipAddrKey := apitypes.NamespacedName{
Namespace: ctx.IONOSCloudMachine.Namespace,
Name: ipAddrName,
}
if err = ctx.K8sClient.Get(ctx, ipAddrKey, ipAddr); err != nil {
return &reconcile.Result{}, err
}

toAdd := fmt.Sprintf("%s/%d", ipAddr.Spec.Address, ipAddr.Spec.Prefix)
parsedPrefix, err := netip.ParsePrefix(toAdd)
if err != nil {
msg := fmt.Sprintf("IPAddress %s/%s has invalid ip address: %q",
ipAddrKey.Namespace,
ipAddrKey.Name,
toAdd,
)
conditions.MarkFalse(ctx.IONOSCloudMachine, v1alpha1.IPAddressCreatedCondition, msg, clusterv1.ConditionSeverityInfo, "")
return &reconcile.Result{RequeueAfter: defaultMachineRetryIntervalOnBusy}, err
}

gatewayAddr, err := netip.ParseAddr(ipAddr.Spec.Gateway)
if err != nil {
msg := fmt.Sprintf("IPAddress %s/%s has invalid gateway: %q",
ipAddrKey.Namespace,
ipAddrKey.Name,
ipAddr.Spec.Gateway,
)
conditions.MarkFalse(ctx.IONOSCloudMachine, v1alpha1.IPAddressCreatedCondition, msg, clusterv1.ConditionSeverityInfo, "")
return &reconcile.Result{RequeueAfter: defaultMachineRetryIntervalOnBusy}, err
}

if parsedPrefix.Addr().Is4() != gatewayAddr.Is4() {
msg := fmt.Sprintf("IPAddress %s/%s has mismatched gateway and address IP families",
ipAddrKey.Namespace,
ipAddrKey.Name,
)
conditions.MarkFalse(ctx.IONOSCloudMachine, v1alpha1.IPAddressCreatedCondition, msg, clusterv1.ConditionSeverityInfo, "")
return &reconcile.Result{RequeueAfter: defaultMachineRetryIntervalOnBusy}, err
}
device.PrimaryIP = ionoscloud.ToPtr(toAdd)
}

conditions.MarkTrue(ctx.IONOSCloudMachine, v1alpha1.IPAddressCreatedCondition)
return nil, nil
}

func (r *IONOSCloudMachineReconciler) reconcileServer(ctx *context.MachineContext) (*reconcile.Result, error) {
ctx.Logger.Info("Reconciling server")

if ctx.IONOSCloudMachine.Spec.ProviderID == "" {
// Get the bootstrap data.
bootstrapData, err := r.getBootstrapData(ctx)
Expand All @@ -360,9 +516,14 @@ func (r *IONOSCloudMachineReconciler) reconcileServer(ctx *context.MachineContex
nics := make([]ionoscloud.Nic, 0)
for _, nic := range ctx.IONOSCloudMachine.Spec.Nics {
lanSpec := ctx.IONOSCloudCluster.Lan(nic.LanRef.Name)
ips := []string{}
if nic.PrimaryIP != nil {
ips = append(ips, *nic.PrimaryIP)
}
nics = append(nics, ionoscloud.Nic{
Properties: &ionoscloud.NicProperties{
Dhcp: ionoscloud.ToPtr(true),
Dhcp: ionoscloud.ToPtr(nic.UseDHCP),
Ips: &ips,
Lan: lanSpec.LanID,
Name: ionoscloud.ToPtr(fmt.Sprintf("%s-nic-%s", ctx.IONOSCloudMachine.Name, lanSpec.Name)),
},
Expand Down

0 comments on commit 76d5080

Please sign in to comment.