Skip to content
Draft
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
7 changes: 7 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ linters-settings:
allow:
- error
- generic
- loadbalancing.Provisioner
- ionoscloud.Client
- bootstrap.ClusterProvider
- framework.ClusterProxy
Expand Down Expand Up @@ -137,6 +138,12 @@ issues:
# changes in PRs and avoid nitpicking.
exclude-use-default: false
exclude-rules:
# TODO(lubedacht): remove this exclusion, once the provisioners are implemented
# I couldn't find a place where to put nolint to disable this linter inline.
- linters:
- dupl
path: "provisioner_(.+).go"
text: "(\\d+)-(\\d+) lines are duplicate of .*"
- linters:
- containedctx
path: '(.+)_test\.go'
Expand Down
9 changes: 9 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,13 @@ resources:
kind: IonosCloudMachineTemplate
path: github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: cluster.x-k8s.io
group: infrastructure
kind: IonosCloudLoadBalancer
path: github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1
version: v1alpha1
version: "3"
228 changes: 228 additions & 0 deletions api/v1alpha1/ionoscloudloadbalancer_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/*
Copyright 2024 IONOS Cloud.

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 v1alpha1

import (
"strconv"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
)

const (
// LoadBalancerFinalizer allows cleanup of resources, which are
// associated with the IonosCloudLoadBalancer before removing it from the API server.
LoadBalancerFinalizer = "ionoscloudloadbalancer.infrastructure.cluster.x-k8s.io"

// LoadBalancerReadyCondition is the condition for the IonosCloudLoadBalancer, which indicates that the load balancer is ready.
LoadBalancerReadyCondition clusterv1.ConditionType = "LoadBalancerReady"

// InvalidEndpointConfigurationReason indicates that the endpoints for IonosCloudCluster and IonosCloudLoadBalancer
// have not been properly configured.
InvalidEndpointConfigurationReason = "InvalidEndpointConfiguration"
)

// IonosCloudLoadBalancerSpec defines the desired state of IonosCloudLoadBalancer.
type IonosCloudLoadBalancerSpec struct {
// LoadBalancerEndpoint represents the endpoint of the load balanced control plane.
// If the endpoint isn't provided, the controller will reserve a new public IP address.
// The port is optional and defaults to 6443.
//
// For external load balancers, the endpoint and port must be provided.
//+kubebuilder:validation:XValidation:rule="self.host == oldSelf.host || oldSelf.host == ''",message="control plane endpoint host cannot be updated"
//+kubebuilder:validation:XValidation:rule="self.port == oldSelf.port || oldSelf.port == 0",message="control plane endpoint port cannot be updated"
LoadBalancerEndpoint clusterv1.APIEndpoint `json:"loadBalancerEndpoint,omitempty"`

// LoadBalancerSource is the actual load balancer definition.
LoadBalancerSource `json:",inline"`
}

// LoadBalancerSource defines the source of the load balancer.
type LoadBalancerSource struct {
// NLB is used for setting up a network load balancer.
//+optional
NLB *NLBSpec `json:"nlb,omitempty"`
}

// NLBSpec defines the spec for a network load balancer.
type NLBSpec struct {
// DatacenterID is the ID of the datacenter where the load balancer should be created.
//+kubebuilder:validation:XValidation:rule="self == oldSelf",message="datacenterID is immutable"
//+kubebuilder:validation:Format=uuid
//+required
DatacenterID string `json:"datacenterID"`

// Algorithm is the load balancing algorithm.
//+kubebuilder:validation:Enum=ROUND_ROBIN;LEAST_CONNECTION;RANDOM;SOURCE_IP
//+kubebuilder:default=ROUND_ROBIN
//+optional
Algorithm string `json:"algorithm,omitempty"`

// Protocol is the load balancing protocol.
//+kubebuilder:validation:Enum=TCP;HTTP
//+kubebuilder:default=TCP
//+optional
Protocol string `json:"protocol,omitempty"`
}

// IonosCloudLoadBalancerStatus defines the observed state of IonosCloudLoadBalancer.
type IonosCloudLoadBalancerStatus struct {
// Ready indicates that the load balancer is ready.
//+optional
Ready bool `json:"ready,omitempty"`

// Conditions defines current service state of the IonosCloudLoadBalancer.
//+optional
Conditions clusterv1.Conditions `json:"conditions,omitempty"`

// CurrentRequest shows the current provisioning request for any
// cloud resource that is being provisioned.
//+optional
CurrentRequest *ProvisioningRequest `json:"currentRequest,omitempty"`

// NLBStatus defines the status for a network load balancer.
//+optional
NLBStatus *NLBStatus `json:"nlbStatus,omitempty"`
}

// NLBStatus holds information about the NLB configuration of the load balancer.
type NLBStatus struct {
// ID is the ID of the network load balancer.
ID string `json:"id,omitempty"`

// PublicLANID is the ID of the LAN used for incoming traffic.
PublicLANID int32 `json:"publicLANID,omitempty"`

// PrivateLANID is the ID of the LAN used for outgoing traffic.
PrivateLANID int32 `json:"privateLANID,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// IonosCloudLoadBalancer is the Schema for the ionoscloudloadbalancers API
// +kubebuilder:resource:path=ionoscloudloadbalancers,scope=Namespaced,categories=cluster-api;ionoscloud,shortName=iclb
type IonosCloudLoadBalancer struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec IonosCloudLoadBalancerSpec `json:"spec,omitempty"`
Status IonosCloudLoadBalancerStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// IonosCloudLoadBalancerList contains a list of IonosCloudLoadBalancer.
type IonosCloudLoadBalancerList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []IonosCloudLoadBalancer `json:"items"`
}

// GetConditions returns the conditions from the status.
func (l *IonosCloudLoadBalancer) GetConditions() clusterv1.Conditions {
return l.Status.Conditions
}

// SetConditions sets the conditions in the status.
func (l *IonosCloudLoadBalancer) SetConditions(conditions clusterv1.Conditions) {
l.Status.Conditions = conditions
}

// SetCurrentRequest sets the current provisioning request.
func (l *IonosCloudLoadBalancer) SetCurrentRequest(method, status, requestPath string) {
l.Status.CurrentRequest = &ProvisioningRequest{
Method: method,
State: status,
RequestPath: requestPath,
}
}

// DeleteCurrentRequest deletes the current provisioning request.
func (l *IonosCloudLoadBalancer) DeleteCurrentRequest() {
l.Status.CurrentRequest = nil
}

// GetNLBID returns the NLB ID from the status.
func (l *IonosCloudLoadBalancer) GetNLBID() string {
if l.Status.NLBStatus == nil {
return ""
}

return l.Status.NLBStatus.ID
}

// SetNLBID sets the NLB ID in the status.
func (l *IonosCloudLoadBalancer) SetNLBID(nlbID string) {
if l.Status.NLBStatus == nil {
l.Status.NLBStatus = &NLBStatus{}
}

l.Status.NLBStatus.ID = nlbID
}

// SetPublicLANID sets the public LAN ID in the status.
func (l *IonosCloudLoadBalancer) SetPublicLANID(id string) error {
if l.Status.NLBStatus == nil {
l.Status.NLBStatus = &NLBStatus{}
}
lanID, err := strconv.ParseInt(id, 10, 32)
if err != nil {
return err
}

l.Status.NLBStatus.PublicLANID = int32(lanID)
return nil
}

// GetPublicLANID returns the public LAN ID from the status.
func (l *IonosCloudLoadBalancer) GetPublicLANID() string {
if l.Status.NLBStatus == nil {
return ""
}

return strconv.Itoa(int(l.Status.NLBStatus.PublicLANID))
}

// SetPrivateLANID sets the private LAN ID in the status.
func (l *IonosCloudLoadBalancer) SetPrivateLANID(id string) error {
if l.Status.NLBStatus == nil {
l.Status.NLBStatus = &NLBStatus{}
}

lanID, err := strconv.ParseInt(id, 10, 32)
if err != nil {
return err
}

l.Status.NLBStatus.PrivateLANID = int32(lanID)

return nil
}

// GetPrivateLANID returns the private LAN ID from the status.
func (l *IonosCloudLoadBalancer) GetPrivateLANID() string {
if l.Status.NLBStatus == nil {
return ""
}

return strconv.Itoa(int(l.Status.NLBStatus.PrivateLANID))
}

func init() {
objectTypes = append(objectTypes, &IonosCloudLoadBalancer{}, &IonosCloudLoadBalancerList{})
}
113 changes: 113 additions & 0 deletions api/v1alpha1/ionoscloudloadbalancer_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
Copyright 2024 IONOS Cloud.

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 v1alpha1

import (
"context"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/client"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

const (
exampleDatacenterID = "fe3b4e3d-3b0e-4e6c-9e3e-4f3c9e3e4f3c"
exampleSecondaryDatacenterID = "fe3b4e3d-3b0e-4e6c-9e3e-4f3c9e3e4f3d"
)

var exampleEndpoint = clusterv1.APIEndpoint{
Host: "example.com",
Port: 6443,
}

func defaultLoadBalancer(source LoadBalancerSource) *IonosCloudLoadBalancer {
return &IonosCloudLoadBalancer{
ObjectMeta: metav1.ObjectMeta{
Name: "test-loadbalancer",
Namespace: metav1.NamespaceDefault,
},
Spec: IonosCloudLoadBalancerSpec{
LoadBalancerSource: source,
},
}
}

var _ = Describe("IonosCloudLoadBalancer", func() {
AfterEach(func() {
err := k8sClient.Delete(context.Background(), defaultLoadBalancer(LoadBalancerSource{}))
Expect(client.IgnoreNotFound(err)).To(Succeed())
})

Context("Create", func() {
When("Using an NLB", func() {
It("Should fail when not providing a datacenter ID", func() {
dlb := defaultLoadBalancer(LoadBalancerSource{NLB: &NLBSpec{}})
Expect(k8sClient.Create(context.Background(), dlb)).NotTo(Succeed())
})
It("Should fail when not providing a uuid for the datacenter ID", func() {
dlb := defaultLoadBalancer(LoadBalancerSource{NLB: &NLBSpec{DatacenterID: "something-invalid"}})
Expect(k8sClient.Create(context.Background(), dlb)).NotTo(Succeed())
})
It("Should succeed when providing a datacenter ID", func() {
dlb := defaultLoadBalancer(LoadBalancerSource{NLB: &NLBSpec{DatacenterID: exampleDatacenterID}})
Expect(k8sClient.Create(context.Background(), dlb)).To(Succeed())
})
It("Should have ROUND_ROBIN as the default algorithm", func() {
dlb := defaultLoadBalancer(LoadBalancerSource{NLB: &NLBSpec{DatacenterID: exampleDatacenterID}})
Expect(k8sClient.Create(context.Background(), dlb)).To(Succeed())
Expect(dlb.Spec.NLB.Algorithm).To(Equal("ROUND_ROBIN"))
})
It("Should fail when providing an invalid algorithm", func() {
dlb := defaultLoadBalancer(LoadBalancerSource{NLB: &NLBSpec{DatacenterID: exampleDatacenterID, Algorithm: "INVALID"}})
Expect(k8sClient.Create(context.Background(), dlb)).NotTo(Succeed())
})
It("Should have TCP as the default protocol", func() {
dlb := defaultLoadBalancer(LoadBalancerSource{NLB: &NLBSpec{DatacenterID: exampleDatacenterID}})
Expect(k8sClient.Create(context.Background(), dlb)).To(Succeed())
Expect(dlb.Spec.NLB.Protocol).To(Equal("TCP"))
})
It("Should fail when providing an invalid protocol", func() {
dlb := defaultLoadBalancer(LoadBalancerSource{NLB: &NLBSpec{DatacenterID: exampleDatacenterID, Protocol: "INVALID"}})
Expect(k8sClient.Create(context.Background(), dlb)).NotTo(Succeed())
})
It("Should succeed providing an endpoint and a port", func() {
dlb := defaultLoadBalancer(LoadBalancerSource{NLB: &NLBSpec{DatacenterID: exampleDatacenterID}})
dlb.Spec.LoadBalancerEndpoint = exampleEndpoint
Expect(k8sClient.Create(context.Background(), dlb)).To(Succeed())
})
It("Should fail when providing a host and a port without a datacenter ID", func() {
dlb := defaultLoadBalancer(LoadBalancerSource{NLB: &NLBSpec{}})
dlb.Spec.LoadBalancerEndpoint = exampleEndpoint
Expect(k8sClient.Create(context.Background(), dlb)).NotTo(Succeed())
})
})
Context("Update", func() {
When("Using an NLB", func() {
It("Should fail when attempting to update the datacenter ID", func() {
dlb := defaultLoadBalancer(LoadBalancerSource{NLB: &NLBSpec{DatacenterID: exampleDatacenterID}})
Expect(k8sClient.Create(context.Background(), dlb)).To(Succeed())

dlb.Spec.NLB.DatacenterID = exampleSecondaryDatacenterID
Expect(k8sClient.Update(context.Background(), dlb)).NotTo(Succeed())
})
})
})
})
})
Loading
Loading