diff --git a/examples/ippool/ippool.yaml b/examples/ippool/ippool.yaml index d7603b351..28fc0589f 100644 --- a/examples/ippool/ippool.yaml +++ b/examples/ippool/ippool.yaml @@ -35,6 +35,8 @@ apiVersion: ipam.metal3.io/v1alpha1 kind: IPClaim metadata: name: ${CLUSTER_NAME}-controlplane-template-0-provisioning-pool + annotations: + ipAddress: spec: pool: name: pool1 @@ -44,6 +46,8 @@ apiVersion: ipam.cluster.x-k8s.io/v1beta1 kind: IPAddressClaim metadata: name: ${CLUSTER_NAME}-controlplane-template-1-provisioning-pool + annotations: + ipAddress: spec: poolRef: apiGroup: ipam.metal3.io diff --git a/ipam/ippool_manager.go b/ipam/ippool_manager.go index 58914b9cd..e8935ad01 100644 --- a/ipam/ippool_manager.go +++ b/ipam/ippool_manager.go @@ -41,6 +41,7 @@ var ( const ( IPAddressClaimFinalizer = "ipam.metal3.io/ipaddressclaim" IPAddressFinalizer = "ipam.metal3.io/ipaddress" + IPAddressAnnotation = "ipAddress" ) // IPPoolManagerInterface is an interface for a IPPoolManager. @@ -415,6 +416,15 @@ func (m *IPPoolManager) allocateAddress(addressClaim *ipamv1.IPClaim, ipAllocated := false + requestedIP := ipamv1.IPAddressStr(addressClaim.ObjectMeta.Annotations[IPAddressAnnotation]) + isRequestedIPAllocated := false + + // Conflict-case, claim is preAllocated but has requested different IP + if requestedIP != "" && ipPreAllocated && requestedIP != preAllocatedAddress { + addressClaim.Status.ErrorMessage = ptr.To("PreAllocation and requested ip address are conflicting") + return "", 0, nil, []ipamv1.IPAddressStr{}, errors.New("PreAllocation and requested ip address are conflicting") + } + for _, pool := range m.IPPool.Spec.Pools { if ipAllocated { break @@ -426,6 +436,13 @@ func (m *IPPoolManager) allocateAddress(addressClaim *ipamv1.IPClaim, break } index++ + // Check if requestedIP is present and matches the current address + if requestedIP != "" && allocatedAddress != requestedIP { + continue + } + if requestedIP != "" && allocatedAddress == requestedIP { + isRequestedIPAllocated = true + } // We have a pre-allocated ip, we just need to ensure that it matches the current address // if it does not, continue and try the next address if ipPreAllocated && allocatedAddress != preAllocatedAddress { @@ -455,6 +472,11 @@ func (m *IPPoolManager) allocateAddress(addressClaim *ipamv1.IPClaim, } } } + // We did not get requestedIp as it did not match with any available IP + if requestedIP != "" && isRequestedIPAllocated && !ipAllocated { + addressClaim.Status.ErrorMessage = ptr.To("Requested IP not available") + return "", 0, nil, []ipamv1.IPAddressStr{}, errors.New("Requested IP not available") + } // We have a preallocated IP but we did not find it in the pools! It means it is // misconfigured if !ipAllocated && ipPreAllocated { @@ -485,6 +507,24 @@ func (m *IPPoolManager) capiAllocateAddress(addressClaim *capipamv1.IPAddressCla ipAllocated := false + requestedIP := ipamv1.IPAddressStr(addressClaim.ObjectMeta.Annotations[IPAddressAnnotation]) + isRequestedIPAllocated := false + + // Conflict-case, claim is preAllocated but has requested different IP + if requestedIP != "" && ipPreAllocated && requestedIP != preAllocatedAddress { + conditions := clusterv1.Conditions{} + conditions = append(conditions, clusterv1.Condition{ + Type: "ErrorMessage", + Status: corev1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Severity: "Error", + Reason: "ErrorMessage", + Message: "PreAllocation and requested ip address are conflicting", + }) + addressClaim.SetConditions(conditions) + return "", 0, nil, errors.New("PreAllocation and requested ip address are conflicting") + } + for _, pool := range m.IPPool.Spec.Pools { if ipAllocated { break @@ -496,6 +536,13 @@ func (m *IPPoolManager) capiAllocateAddress(addressClaim *capipamv1.IPAddressCla break } index++ + // Check if requestedIP is present and matches the current address + if requestedIP != "" && allocatedAddress != requestedIP { + continue + } + if requestedIP != "" && allocatedAddress == requestedIP { + isRequestedIPAllocated = true + } // We have a pre-allocated ip, we just need to ensure that it matches the current address // if it does not, continue and try the next address if ipPreAllocated && allocatedAddress != preAllocatedAddress { @@ -522,6 +569,20 @@ func (m *IPPoolManager) capiAllocateAddress(addressClaim *capipamv1.IPAddressCla } } } + // We did not get requestedIp as it did not match with any available IP + if requestedIP != "" && isRequestedIPAllocated && !ipAllocated { + conditions := clusterv1.Conditions{} + conditions = append(conditions, clusterv1.Condition{ + Type: "ErrorMessage", + Status: corev1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Severity: "Error", + Reason: "ErrorMessage", + Message: "Requested IP not available", + }) + addressClaim.SetConditions(conditions) + return "", 0, nil, errors.New("Requested IP not available") + } // We have a preallocated IP but we did not find it in the pools! It means it is // misconfigured if !ipAllocated && ipPreAllocated { diff --git a/ipam/ippool_manager_test.go b/ipam/ippool_manager_test.go index 2720c564b..515c20184 100644 --- a/ipam/ippool_manager_test.go +++ b/ipam/ippool_manager_test.go @@ -1916,6 +1916,125 @@ var _ = Describe("IPPool manager", func() { expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), expectedPrefix: 24, }), + Entry("One pool, with start and existing address, ipAddress annotation present", testCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + }, + }, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + }, + }, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + Annotations: map[string]string{ + IPAddressAnnotation: "192.168.0.16", + }, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.12"): "bcde", + ipamv1.IPAddressStr("192.168.0.11"): "abcd", + }, + expectedAddress: ipamv1.IPAddressStr("192.168.0.16"), + expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + expectedPrefix: 24, + }), + Entry("One pool, with start and existing address, ipAddress annotation present but already acquired", testCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + }, + }, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + }, + }, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + Annotations: map[string]string{ + IPAddressAnnotation: "192.168.0.11", + }, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.12"): "bcde", + ipamv1.IPAddressStr("192.168.0.11"): "abcd", + }, + expectError: true, + }), + + Entry("One pool, with start and existing address, ipAddress annotation present and requested ipAddress in preAllocations with conflicting ipclaim name", testCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + }, + }, + Prefix: 24, + PreAllocations: map[string]ipamv1.IPAddressStr{ + "TestRef": ipamv1.IPAddressStr("192.168.0.15"), + }, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + }, + }, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + Annotations: map[string]string{ + IPAddressAnnotation: "192.168.0.16", + }, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.12"): "bcde", + ipamv1.IPAddressStr("192.168.0.11"): "abcd", + }, + expectError: true, + }), + Entry("One pool, with start and existing address, ipAddress annotation present and requested ipAddress in preAllocations with non-conflicting ipclaim name", testCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + }, + }, + Prefix: 24, + PreAllocations: map[string]ipamv1.IPAddressStr{ + "TestRef": ipamv1.IPAddressStr("192.168.0.16"), + }, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + }, + }, + ipClaim: &ipamv1.IPClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + Annotations: map[string]string{ + IPAddressAnnotation: "192.168.0.16", + }, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.12"): "bcde", + ipamv1.IPAddressStr("192.168.0.11"): "abcd", + }, + expectedAddress: ipamv1.IPAddressStr("192.168.0.16"), + expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + expectedPrefix: 24, + }), Entry("One pool, with subnet and override prefix", testCaseAllocateAddress{ ipPool: &ipamv1.IPPool{ Spec: ipamv1.IPPoolSpec{ @@ -2230,6 +2349,124 @@ var _ = Describe("IPPool manager", func() { expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), expectedPrefix: 24, }), + Entry("One pool, with start and existing address, ipAddress annotation", testCapiCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + }, + }, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + Annotations: map[string]string{ + IPAddressAnnotation: "192.168.0.16", + }, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.12"): "bcde", + ipamv1.IPAddressStr("192.168.0.11"): "abcd", + }, + expectedAddress: ipamv1.IPAddressStr("192.168.0.16"), + expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + expectedPrefix: 24, + }), + Entry("One pool, with start and existing address, ipAddress annotation but already acquired", testCapiCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + }, + }, + Prefix: 24, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + Annotations: map[string]string{ + IPAddressAnnotation: "192.168.0.12", + }, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.12"): "bcde", + ipamv1.IPAddressStr("192.168.0.11"): "abcd", + }, + expectError: true, + }), + Entry("One pool, with start and existing address, ipAddress annotation present and requested ipAddress in preAllocations with conflicting ipclaim name", testCapiCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + }, + }, + Prefix: 24, + PreAllocations: map[string]ipamv1.IPAddressStr{ + "TestRef": ipamv1.IPAddressStr("192.168.0.15"), + }, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + Annotations: map[string]string{ + IPAddressAnnotation: "192.168.0.16", + }, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.12"): "bcde", + ipamv1.IPAddressStr("192.168.0.11"): "abcd", + }, + expectError: true, + }), + Entry("One pool, with start and existing address, ipAddress annotation present and requested ipAddress in preAllocations with non-conflicting ipclaim name", testCapiCaseAllocateAddress{ + ipPool: &ipamv1.IPPool{ + Spec: ipamv1.IPPoolSpec{ + Pools: []ipamv1.Pool{ + { + Start: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.11")), + End: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.20")), + }, + }, + Prefix: 24, + PreAllocations: map[string]ipamv1.IPAddressStr{ + "TestRef": ipamv1.IPAddressStr("192.168.0.16"), + }, + Gateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + }, + }, + ipAddressClaim: &capipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "TestRef", + Annotations: map[string]string{ + IPAddressAnnotation: "192.168.0.16", + }, + }, + }, + addresses: map[ipamv1.IPAddressStr]string{ + ipamv1.IPAddressStr("192.168.0.12"): "bcde", + ipamv1.IPAddressStr("192.168.0.11"): "abcd", + }, + expectedAddress: ipamv1.IPAddressStr("192.168.0.16"), + expectedGateway: (*ipamv1.IPAddressStr)(ptr.To("192.168.0.1")), + expectedPrefix: 24, + }), Entry("One pool, with subnet and override prefix", testCapiCaseAllocateAddress{ ipPool: &ipamv1.IPPool{ Spec: ipamv1.IPPoolSpec{