Skip to content

Commit 075f649

Browse files
authored
✨ Add IPAM for nodes (#142)
**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)
1 parent 33a3f06 commit 075f649

21 files changed

+1744
-66
lines changed

api/v1alpha1/ionoscloudmachine_types.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,12 @@ type IonosCloudMachineSpec struct {
151151
Disk *Volume `json:"disk"`
152152

153153
// AdditionalNetworks defines the additional network configurations for the VM.
154+
//
154155
//+optional
155-
AdditionalNetworks Networks `json:"additionalNetworks,omitempty"`
156+
AdditionalNetworks []Network `json:"additionalNetworks,omitempty"`
157+
158+
// IPAMConfig allows to obtain IP Addresses from existing IP pools instead of using DHCP.
159+
IPAMConfig `json:",inline"`
156160

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

175-
// Networks contains a list of additional LAN IDs
176-
// that should be attached to the VM.
177-
type Networks []Network
178-
179179
// Network contains the config for additional LANs.
180180
type Network struct {
181181
// NetworkID represents an ID an existing LAN in the data center.
@@ -192,6 +192,9 @@ type Network struct {
192192
//+kubebuilder:default=true
193193
//+optional
194194
DHCP *bool `json:"dhcp,omitempty"`
195+
196+
// IPAMConfig allows to obtain IP Addresses from existing IP pools instead of using DHCP.
197+
IPAMConfig `json:",inline"`
195198
}
196199

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

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

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

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

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

332343
// NetworkID is the ID of the LAN to which the NIC is connected.
333344
NetworkID int32 `json:"networkID"`

api/v1alpha1/ionoscloudmachine_types_test.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func defaultMachine() *IonosCloudMachine {
5555
ID: "1eef-48ec-a246-a51a33aa4f3a",
5656
},
5757
},
58-
AdditionalNetworks: Networks{
58+
AdditionalNetworks: []Network{
5959
{
6060
NetworkID: 1,
6161
},
@@ -64,6 +64,20 @@ func defaultMachine() *IonosCloudMachine {
6464
}
6565
}
6666

67+
func setInvalidPoolRef(m *IonosCloudMachine, poolType string, kind, apiGroup, name string) {
68+
ref := &corev1.TypedLocalObjectReference{
69+
APIGroup: ptr.To(apiGroup),
70+
Kind: kind,
71+
Name: name,
72+
}
73+
switch poolType {
74+
case "IPv6":
75+
m.Spec.AdditionalNetworks[0].IPv6PoolRef = ref
76+
case "IPv4":
77+
m.Spec.AdditionalNetworks[0].IPv4PoolRef = ref
78+
}
79+
}
80+
6781
var _ = Describe("IonosCloudMachine Tests", func() {
6882
AfterEach(func() {
6983
m := &IonosCloudMachine{
@@ -354,6 +368,43 @@ var _ = Describe("IonosCloudMachine Tests", func() {
354368
m.Spec.AdditionalNetworks[0].NetworkID = -1
355369
Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed())
356370
})
371+
DescribeTable("should allow IPv4PoolRef.Kind GlobalInClusterIPPool and InClusterIPPool", func(kind string) {
372+
m := defaultMachine()
373+
m.Spec.AdditionalNetworks[0].IPv4PoolRef = &corev1.TypedLocalObjectReference{
374+
APIGroup: ptr.To("ipam.cluster.x-k8s.io"),
375+
Kind: kind,
376+
Name: "ipv4-pool",
377+
}
378+
Expect(k8sClient.Create(context.Background(), m)).To(Succeed())
379+
},
380+
Entry("GlobalInClusterIPPool", "GlobalInClusterIPPool"),
381+
Entry("InClusterIPPool", "InClusterIPPool"),
382+
)
383+
DescribeTable("should allow IPv6PoolRef.Kind GlobalInClusterIPPool and InClusterIPPool", func(kind string) {
384+
m := defaultMachine()
385+
m.Spec.AdditionalNetworks[0].IPv6PoolRef = &corev1.TypedLocalObjectReference{
386+
APIGroup: ptr.To("ipam.cluster.x-k8s.io"),
387+
Kind: kind,
388+
Name: "ipv6-pool",
389+
}
390+
Expect(k8sClient.Create(context.Background(), m)).To(Succeed())
391+
},
392+
Entry("GlobalInClusterIPPool", "GlobalInClusterIPPool"),
393+
Entry("InClusterIPPool", "InClusterIPPool"),
394+
)
395+
DescribeTable("must not allow invalid pool references",
396+
func(poolType, kind, apiGroup, name string) {
397+
m := defaultMachine()
398+
setInvalidPoolRef(m, poolType, kind, apiGroup, name)
399+
Expect(k8sClient.Create(context.Background(), m)).ToNot(Succeed())
400+
},
401+
Entry("invalid IPv6PoolRef with invalid kind", "IPv6", "SomeOtherIPPoolKind", "ipam.cluster.x-k8s.io", "ipv6-pool"),
402+
Entry("invalid IPv6PoolRef with invalid apiGroup", "IPv6", "InClusterIPPool", "SomeWrongAPIGroup", "ipv6-pool"),
403+
Entry("invalid IPv6PoolRef with empty name", "IPv6", "InClusterIPPool", "ipam.cluster.x-k8s.io", ""),
404+
Entry("invalid IPv4PoolRef with invalid kind", "IPv4", "SomeOtherIPPoolKind", "ipam.cluster.x-k8s.io", "ipv4-pool"),
405+
Entry("invalid IPv4PoolRef with invalid apiGroup", "IPv4", "InClusterIPPool", "SomeWrongAPIGroup", "ipv4-pool"),
406+
Entry("invalid IPv4PoolRef with empty name", "IPv4", "InClusterIPPool", "ipam.cluster.x-k8s.io", ""),
407+
)
357408
It("DHCP should default to true", func() {
358409
m := defaultMachine()
359410
Expect(k8sClient.Create(context.Background(), m)).To(Succeed())

api/v1alpha1/suite_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"testing"
2222

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

5455
scheme := runtime.NewScheme()
5556
Expect(AddToScheme(scheme)).To(Succeed())
57+
Expect(ipamv1.AddToScheme(scheme)).To(Succeed())
5658

5759
cfg, err := testEnv.Start()
5860
Expect(err).ToNot(HaveOccurred())

api/v1alpha1/types.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ limitations under the License.
1616

1717
package v1alpha1
1818

19+
import corev1 "k8s.io/api/core/v1"
20+
1921
// ProvisioningRequest is a definition of a provisioning request
2022
// in the IONOS Cloud.
2123
type ProvisioningRequest struct {
@@ -30,3 +32,22 @@ type ProvisioningRequest struct {
3032
//+optional
3133
State string `json:"state,omitempty"`
3234
}
35+
36+
// IPAMConfig optionally defines which IP Pools to use.
37+
type IPAMConfig struct {
38+
// IPv4PoolRef is a reference to an IPAMConfig Pool resource, which exposes IPv4 addresses.
39+
// The NIC will use an available IP address from the referenced pool.
40+
// +kubebuilder:validation:XValidation:rule="self.apiGroup == 'ipam.cluster.x-k8s.io'",message="ipv4PoolRef allows only IPAMConfig apiGroup ipam.cluster.x-k8s.io"
41+
// +kubebuilder:validation:XValidation:rule="self.kind == 'InClusterIPPool' || self.kind == 'GlobalInClusterIPPool'",message="ipv4PoolRef allows either InClusterIPPool or GlobalInClusterIPPool"
42+
// +kubebuilder:validation:XValidation:rule="self.name != ''",message="ipv4PoolRef.name is required"
43+
// +optional
44+
IPv4PoolRef *corev1.TypedLocalObjectReference `json:"ipv4PoolRef,omitempty"`
45+
46+
// IPv6PoolRef is a reference to an IPAMConfig pool resource, which exposes IPv6 addresses.
47+
// The NIC will use an available IP address from the referenced pool.
48+
// +kubebuilder:validation:XValidation:rule="self.apiGroup == 'ipam.cluster.x-k8s.io'",message="ipv6PoolRef allows only IPAMConfig apiGroup ipam.cluster.x-k8s.io"
49+
// +kubebuilder:validation:XValidation:rule="self.kind == 'InClusterIPPool' || self.kind == 'GlobalInClusterIPPool'",message="ipv6PoolRef allows either InClusterIPPool or GlobalInClusterIPPool"
50+
// +kubebuilder:validation:XValidation:rule="self.name != ''",message="ipv6PoolRef.name is required"
51+
// +optional
52+
IPv6PoolRef *corev1.TypedLocalObjectReference `json:"ipv6PoolRef,omitempty"`
53+
}

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 28 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
2929
"k8s.io/klog/v2"
3030
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
31+
ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1"
3132
"sigs.k8s.io/cluster-api/util/flags"
3233
ctrl "sigs.k8s.io/controller-runtime"
3334
"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -53,6 +54,8 @@ func init() {
5354

5455
utilruntime.Must(clusterv1.AddToScheme(scheme))
5556
utilruntime.Must(infrav1.AddToScheme(scheme))
57+
utilruntime.Must(ipamv1.AddToScheme(scheme))
58+
5659
//+kubebuilder:scaffold:scheme
5760
}
5861

0 commit comments

Comments
 (0)