Skip to content

Commit

Permalink
✨ Add IPAM for nodes (#142)
Browse files Browse the repository at this point in the history
**What is the purpose of this pull request/Why do we need it?**
We would like to get IPs from a fixed pool of IPs instead of relying on
the DHCP.


**Issue #, if available:**
#130

**Description of changes:**
- added IPv4PoolRef/IPv6PoolRef to both IonosCloudMachine and Network
- workflow: IonosCloudMachine controller checks for PoolRefs and creates
IPAddressClaims when needed. It then waits for an external controller to
create IPAddress objects from the IPAddressClaim. Then it uses the IP
from the IPAddress object to create a server via Ionos cloud api.

**Special notes for your reviewer:**
I did not write tests yet as I am waiting for #137 to be merged and I'd
like to get some feedback about this PR first.
I am also unsure where I should put the docs, I did not find anything
for the other api stuff beside the api definition itself. Maybe this is
already enough?

**Checklist:**
- [ ] Documentation updated
- [x] Unit Tests added
- [ ] E2E Tests added
- [x] Includes
[emojis](https://github.com/kubernetes-sigs/kubebuilder-release-tools?tab=readme-ov-file#kubebuilder-project-versioning)
  • Loading branch information
Mattes83 authored Aug 30, 2024
1 parent 33a3f06 commit 075f649
Show file tree
Hide file tree
Showing 21 changed files with 1,744 additions and 66 deletions.
27 changes: 19 additions & 8 deletions api/v1alpha1/ionoscloudmachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,12 @@ type IonosCloudMachineSpec struct {
Disk *Volume `json:"disk"`

// AdditionalNetworks defines the additional network configurations for the VM.
//
//+optional
AdditionalNetworks Networks `json:"additionalNetworks,omitempty"`
AdditionalNetworks []Network `json:"additionalNetworks,omitempty"`

// IPAMConfig allows to obtain IP Addresses from existing IP pools instead of using DHCP.
IPAMConfig `json:",inline"`

// FailoverIP can be set to enable failover for VMs in the same MachineDeployment.
// It can be either set to an already reserved IPv4 address, or it can be set to "AUTO"
Expand All @@ -172,10 +176,6 @@ type IonosCloudMachineSpec struct {
Type ServerType `json:"type,omitempty"`
}

// Networks contains a list of additional LAN IDs
// that should be attached to the VM.
type Networks []Network

// Network contains the config for additional LANs.
type Network struct {
// NetworkID represents an ID an existing LAN in the data center.
Expand All @@ -192,6 +192,9 @@ type Network struct {
//+kubebuilder:default=true
//+optional
DHCP *bool `json:"dhcp,omitempty"`

// IPAMConfig allows to obtain IP Addresses from existing IP pools instead of using DHCP.
IPAMConfig `json:",inline"`
}

// Volume is the physical storage on the VM.
Expand Down Expand Up @@ -259,7 +262,7 @@ type IonosCloudMachineStatus struct {
Ready bool `json:"ready"`

// MachineNetworkInfo contains information about the network configuration of the VM.
// This information is only available after the VM has been provisioned.
//+optional
MachineNetworkInfo *MachineNetworkInfo `json:"machineNetworkInfo,omitempty"`

// FailureReason will be set in the event that there is a terminal problem
Expand Down Expand Up @@ -315,6 +318,8 @@ type IonosCloudMachineStatus struct {
}

// MachineNetworkInfo contains information about the network configuration of the VM.
// Before the provisioning MachineNetworkInfo may contain IP addresses to be used for provisioning.
// After provisioning this information is available completely.
type MachineNetworkInfo struct {
// NICInfo holds information about the NICs, which are attached to the VM.
//+optional
Expand All @@ -324,10 +329,16 @@ type MachineNetworkInfo struct {
// NICInfo provides information about the NIC of the VM.
type NICInfo struct {
// IPv4Addresses contains the IPv4 addresses of the NIC.
IPv4Addresses []string `json:"ipv4Addresses"`
// By default, we enable dual-stack, but as we are storing the IP obtained from AddressClaims here before
// creating the VM this can be temporarily empty, e.g. we use DHCP for IPv4 and fixed IP for IPv6.
//+optional
IPv4Addresses []string `json:"ipv4Addresses,omitempty"`

// IPv6Addresses contains the IPv6 addresses of the NIC.
IPv6Addresses []string `json:"ipv6Addresses"`
// By default, we enable dual-stack, but as we are storing the IP obtained from AddressClaims here before
// creating the VM this can be temporarily empty, e.g. we use DHCP for IPv6 and fixed IP for IPv4.
//+optional
IPv6Addresses []string `json:"ipv6Addresses,omitempty"`

// NetworkID is the ID of the LAN to which the NIC is connected.
NetworkID int32 `json:"networkID"`
Expand Down
53 changes: 52 additions & 1 deletion api/v1alpha1/ionoscloudmachine_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func defaultMachine() *IonosCloudMachine {
ID: "1eef-48ec-a246-a51a33aa4f3a",
},
},
AdditionalNetworks: Networks{
AdditionalNetworks: []Network{
{
NetworkID: 1,
},
Expand All @@ -64,6 +64,20 @@ func defaultMachine() *IonosCloudMachine {
}
}

func setInvalidPoolRef(m *IonosCloudMachine, poolType string, kind, apiGroup, name string) {
ref := &corev1.TypedLocalObjectReference{
APIGroup: ptr.To(apiGroup),
Kind: kind,
Name: name,
}
switch poolType {
case "IPv6":
m.Spec.AdditionalNetworks[0].IPv6PoolRef = ref
case "IPv4":
m.Spec.AdditionalNetworks[0].IPv4PoolRef = ref
}
}

var _ = Describe("IonosCloudMachine Tests", func() {
AfterEach(func() {
m := &IonosCloudMachine{
Expand Down Expand Up @@ -354,6 +368,43 @@ var _ = Describe("IonosCloudMachine Tests", func() {
m.Spec.AdditionalNetworks[0].NetworkID = -1
Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed())
})
DescribeTable("should allow IPv4PoolRef.Kind GlobalInClusterIPPool and InClusterIPPool", func(kind string) {
m := defaultMachine()
m.Spec.AdditionalNetworks[0].IPv4PoolRef = &corev1.TypedLocalObjectReference{
APIGroup: ptr.To("ipam.cluster.x-k8s.io"),
Kind: kind,
Name: "ipv4-pool",
}
Expect(k8sClient.Create(context.Background(), m)).To(Succeed())
},
Entry("GlobalInClusterIPPool", "GlobalInClusterIPPool"),
Entry("InClusterIPPool", "InClusterIPPool"),
)
DescribeTable("should allow IPv6PoolRef.Kind GlobalInClusterIPPool and InClusterIPPool", func(kind string) {
m := defaultMachine()
m.Spec.AdditionalNetworks[0].IPv6PoolRef = &corev1.TypedLocalObjectReference{
APIGroup: ptr.To("ipam.cluster.x-k8s.io"),
Kind: kind,
Name: "ipv6-pool",
}
Expect(k8sClient.Create(context.Background(), m)).To(Succeed())
},
Entry("GlobalInClusterIPPool", "GlobalInClusterIPPool"),
Entry("InClusterIPPool", "InClusterIPPool"),
)
DescribeTable("must not allow invalid pool references",
func(poolType, kind, apiGroup, name string) {
m := defaultMachine()
setInvalidPoolRef(m, poolType, kind, apiGroup, name)
Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed())
},
Entry("invalid IPv6PoolRef with invalid kind", "IPv6", "SomeOtherIPPoolKind", "ipam.cluster.x-k8s.io", "ipv6-pool"),
Entry("invalid IPv6PoolRef with invalid apiGroup", "IPv6", "InClusterIPPool", "SomeWrongAPIGroup", "ipv6-pool"),
Entry("invalid IPv6PoolRef with empty name", "IPv6", "InClusterIPPool", "ipam.cluster.x-k8s.io", ""),
Entry("invalid IPv4PoolRef with invalid kind", "IPv4", "SomeOtherIPPoolKind", "ipam.cluster.x-k8s.io", "ipv4-pool"),
Entry("invalid IPv4PoolRef with invalid apiGroup", "IPv4", "InClusterIPPool", "SomeWrongAPIGroup", "ipv4-pool"),
Entry("invalid IPv4PoolRef with empty name", "IPv4", "InClusterIPPool", "ipam.cluster.x-k8s.io", ""),
)
It("DHCP should default to true", func() {
m := defaultMachine()
Expect(k8sClient.Create(context.Background(), m)).To(Succeed())
Expand Down
2 changes: 2 additions & 0 deletions api/v1alpha1/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"testing"

"k8s.io/apimachinery/pkg/runtime"
ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
Expand Down Expand Up @@ -53,6 +54,7 @@ var _ = BeforeSuite(func() {

scheme := runtime.NewScheme()
Expect(AddToScheme(scheme)).To(Succeed())
Expect(ipamv1.AddToScheme(scheme)).To(Succeed())

cfg, err := testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expand Down
21 changes: 21 additions & 0 deletions api/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ limitations under the License.

package v1alpha1

import corev1 "k8s.io/api/core/v1"

// ProvisioningRequest is a definition of a provisioning request
// in the IONOS Cloud.
type ProvisioningRequest struct {
Expand All @@ -30,3 +32,22 @@ type ProvisioningRequest struct {
//+optional
State string `json:"state,omitempty"`
}

// IPAMConfig optionally defines which IP Pools to use.
type IPAMConfig struct {
// IPv4PoolRef is a reference to an IPAMConfig Pool resource, which exposes IPv4 addresses.
// The NIC will use an available IP address from the referenced pool.
// +kubebuilder:validation:XValidation:rule="self.apiGroup == 'ipam.cluster.x-k8s.io'",message="ipv4PoolRef allows only IPAMConfig apiGroup ipam.cluster.x-k8s.io"
// +kubebuilder:validation:XValidation:rule="self.kind == 'InClusterIPPool' || self.kind == 'GlobalInClusterIPPool'",message="ipv4PoolRef allows either InClusterIPPool or GlobalInClusterIPPool"
// +kubebuilder:validation:XValidation:rule="self.name != ''",message="ipv4PoolRef.name is required"
// +optional
IPv4PoolRef *corev1.TypedLocalObjectReference `json:"ipv4PoolRef,omitempty"`

// IPv6PoolRef is a reference to an IPAMConfig pool resource, which exposes IPv6 addresses.
// The NIC will use an available IP address from the referenced pool.
// +kubebuilder:validation:XValidation:rule="self.apiGroup == 'ipam.cluster.x-k8s.io'",message="ipv6PoolRef allows only IPAMConfig apiGroup ipam.cluster.x-k8s.io"
// +kubebuilder:validation:XValidation:rule="self.kind == 'InClusterIPPool' || self.kind == 'GlobalInClusterIPPool'",message="ipv6PoolRef allows either InClusterIPPool or GlobalInClusterIPPool"
// +kubebuilder:validation:XValidation:rule="self.name != ''",message="ipv6PoolRef.name is required"
// +optional
IPv6PoolRef *corev1.TypedLocalObjectReference `json:"ipv6PoolRef,omitempty"`
}
50 changes: 28 additions & 22 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/klog/v2"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1"
"sigs.k8s.io/cluster-api/util/flags"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/controller"
Expand All @@ -53,6 +54,8 @@ func init() {

utilruntime.Must(clusterv1.AddToScheme(scheme))
utilruntime.Must(infrav1.AddToScheme(scheme))
utilruntime.Must(ipamv1.AddToScheme(scheme))

//+kubebuilder:scaffold:scheme
}

Expand Down
Loading

0 comments on commit 075f649

Please sign in to comment.