Skip to content

Commit 678b6c0

Browse files
authored
Merge pull request #171 from adobley/reserve-network-broadcast-addrs
🐛 Controller does not allocate reserved addresses
2 parents 80ec908 + 96dbef6 commit 678b6c0

13 files changed

+511
-171
lines changed

api/v1alpha1/conversion.go

+4
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,7 @@ func Convert_v1alpha1_InClusterIPPoolSpec_To_v1alpha2_InClusterIPPoolSpec(in *In
5757

5858
return nil
5959
}
60+
61+
func Convert_v1alpha2_InClusterIPPoolSpec_To_v1alpha1_InClusterIPPoolSpec(in *v1alpha2.InClusterIPPoolSpec, out *InClusterIPPoolSpec, s conversion.Scope) error {
62+
return autoConvert_v1alpha2_InClusterIPPoolSpec_To_v1alpha1_InClusterIPPoolSpec(in, out, s)
63+
}

api/v1alpha1/zz_generated.conversion.go

+6-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1alpha2/inclusterippool_types.go

+8
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ type InClusterIPPoolSpec struct {
3333
// Gateway
3434
// +optional
3535
Gateway string `json:"gateway,omitempty"`
36+
37+
// AllocateReservedIPAddresses causes the provider to allocate the network
38+
// address (the first address in the inferred subnet) and broadcast address
39+
// (the last address in the inferred subnet) when IPv4. The provider will
40+
// allocate the anycast address address (the first address in the inferred
41+
// subnet) when IPv6.
42+
// +optional
43+
AllocateReservedIPAddresses bool `json:"allocateReservedIPAddresses,omitempty"`
3644
}
3745

3846
// InClusterIPPoolStatus defines the observed state of InClusterIPPool.

config/crd/bases/ipam.cluster.x-k8s.io_globalinclusterippools.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@ spec:
174174
items:
175175
type: string
176176
type: array
177+
allocateReservedIPAddresses:
178+
description: AllocateReservedIPAddresses causes the provider to allocate
179+
the network address (the first address in the inferred subnet) and
180+
broadcast address (the last address in the inferred subnet) when
181+
IPv4. The provider will allocate the anycast address address (the
182+
first address in the inferred subnet) when IPv6.
183+
type: boolean
177184
gateway:
178185
description: Gateway
179186
type: string

config/crd/bases/ipam.cluster.x-k8s.io_inclusterippools.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@ spec:
174174
items:
175175
type: string
176176
type: array
177+
allocateReservedIPAddresses:
178+
description: AllocateReservedIPAddresses causes the provider to allocate
179+
the network address (the first address in the inferred subnet) and
180+
broadcast address (the last address in the inferred subnet) when
181+
IPv4. The provider will allocate the anycast address address (the
182+
first address in the inferred subnet) when IPv6.
183+
type: boolean
177184
gateway:
178185
description: Gateway
179186
type: string

internal/controllers/inclusterippool.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ func genericReconcile(ctx context.Context, c client.Client, pool pooltypes.Gener
197197
return ctrl.Result{}, nil
198198
}
199199

200-
poolIPSet, err := poolutil.AddressesToIPSet(pool.PoolSpec().Addresses)
200+
poolIPSet, err := poolutil.PoolSpecToIPSet(pool.PoolSpec())
201201
if err != nil {
202202
return ctrl.Result{}, errors.Wrap(err, "failed to build ip set from pool spec")
203203
}

internal/controllers/inclusterippool_test.go

+41-15
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ var _ = Describe("IP Pool Reconciler", func() {
5656
})
5757

5858
DescribeTable("it shows the total, used, free ip addresses in the pool",
59-
func(poolType string, addresses []string, gateway string, expectedTotal, expectedUsed, expectedFree int) {
60-
genericPool = newPool(poolType, testPool, namespace, gateway, addresses, 24)
59+
func(poolType string, prefix int, addresses []string, gateway string, expectedTotal, expectedUsed, expectedFree int) {
60+
genericPool = newPool(poolType, testPool, namespace, gateway, addresses, prefix)
6161
Expect(k8sClient.Create(context.Background(), genericPool)).To(Succeed())
6262

6363
Eventually(Object(genericPool)).
@@ -94,34 +94,47 @@ var _ = Describe("IP Pool Reconciler", func() {
9494
},
9595

9696
Entry("When there is 1 claim and no gateway - InClusterIPPool",
97-
"InClusterIPPool", []string{"10.0.0.10-10.0.0.20"}, "", 11, 1, 10),
97+
"InClusterIPPool", 24, []string{"10.0.0.10-10.0.0.20"}, "", 11, 1, 10),
9898
Entry("When there are 2 claims and no gateway - InClusterIPPool",
99-
"InClusterIPPool", []string{"10.0.0.10-10.0.0.20"}, "", 11, 2, 9),
99+
"InClusterIPPool", 24, []string{"10.0.0.10-10.0.0.20"}, "", 11, 2, 9),
100100
Entry("When there is 1 claim with gateway in range - InClusterIPPool",
101-
"InClusterIPPool", []string{"10.0.0.10-10.0.0.20"}, "10.0.0.10", 10, 1, 9),
101+
"InClusterIPPool", 24, []string{"10.0.0.10-10.0.0.20"}, "10.0.0.10", 10, 1, 9),
102102
Entry("When there are 2 claims with gateway in range - InClusterIPPool",
103-
"InClusterIPPool", []string{"10.0.0.10-10.0.0.20"}, "10.0.0.10", 10, 2, 8),
103+
"InClusterIPPool", 24, []string{"10.0.0.10-10.0.0.20"}, "10.0.0.10", 10, 2, 8),
104104
Entry("When there is 1 claim with gateway outside of range - InClusterIPPool",
105-
"InClusterIPPool", []string{"10.0.0.10-10.0.0.20"}, "10.0.0.1", 11, 1, 10),
105+
"InClusterIPPool", 24, []string{"10.0.0.10-10.0.0.20"}, "10.0.0.1", 11, 1, 10),
106+
Entry("When the addresses range includes network addr, it is not available - InClusterIPPool",
107+
"InClusterIPPool", 24, []string{"10.0.0.0-10.0.0.1"}, "10.0.0.2", 1, 1, 0),
108+
Entry("When the addresses range includes broadcast, it is not available - InClusterIPPool",
109+
"InClusterIPPool", 24, []string{"10.0.0.254-10.0.0.255"}, "10.0.0.1", 1, 1, 0),
110+
Entry("When the addresses range is IPv6 and the last range in the subnet, it is available - InClusterIPPool",
111+
"InClusterIPPool", 120, []string{"fe80::ffff"}, "fe80::a", 1, 1, 0),
106112

107113
Entry("When there is 1 claim and no gateway - GlobalInClusterIPPool",
108-
"GlobalInClusterIPPool", []string{"10.0.0.10-10.0.0.20"}, "", 11, 1, 10),
114+
"GlobalInClusterIPPool", 24, []string{"10.0.0.10-10.0.0.20"}, "", 11, 1, 10),
109115
Entry("When there are 2 claims and no gateway - GlobalInClusterIPPool",
110-
"GlobalInClusterIPPool", []string{"10.0.0.10-10.0.0.20"}, "", 11, 2, 9),
116+
"GlobalInClusterIPPool", 24, []string{"10.0.0.10-10.0.0.20"}, "", 11, 2, 9),
111117
Entry("When there is 1 claim with gateway in range - GlobalInClusterIPPool",
112-
"GlobalInClusterIPPool", []string{"10.0.0.10-10.0.0.20"}, "10.0.0.10", 10, 1, 9),
118+
"GlobalInClusterIPPool", 24, []string{"10.0.0.10-10.0.0.20"}, "10.0.0.10", 10, 1, 9),
113119
Entry("When there are 2 claims with gateway in range - GlobalInClusterIPPool",
114-
"GlobalInClusterIPPool", []string{"10.0.0.10-10.0.0.20"}, "10.0.0.10", 10, 2, 8),
120+
"GlobalInClusterIPPool", 24, []string{"10.0.0.10-10.0.0.20"}, "10.0.0.10", 10, 2, 8),
115121
Entry("When there is 1 claim with gateway outside of range - GlobalInClusterIPPool",
116-
"GlobalInClusterIPPool", []string{"10.0.0.10-10.0.0.20"}, "10.0.0.1", 11, 1, 10),
122+
"GlobalInClusterIPPool", 24, []string{"10.0.0.10-10.0.0.20"}, "10.0.0.1", 11, 1, 10),
123+
Entry("When the addresses range includes network addr, it is not available - GlobalInClusterIPPool",
124+
"GlobalInClusterIPPool", 24, []string{"10.0.0.0-10.0.0.1"}, "10.0.0.2", 1, 1, 0),
125+
Entry("When the addresses range includes broadcast, it is not available - GlobalInClusterIPPool",
126+
"GlobalInClusterIPPool", 24, []string{"10.0.0.254-10.0.0.255"}, "10.0.0.1", 1, 1, 0),
127+
Entry("When the addresses range is IPv6 and the last range in the subnet, it is available - GlobalInClusterIPPool",
128+
"GlobalInClusterIPPool", 120, []string{"fe80::ffff"}, "fe80::a", 1, 1, 0),
117129
)
118130

119131
DescribeTable("it shows the out of range ips if any",
120132
func(poolType string, addresses []string, gateway string, updatedAddresses []string, numClaims, expectedOutOfRange int) {
121133
poolSpec := v1alpha2.InClusterIPPoolSpec{
122-
Prefix: 24,
123-
Gateway: gateway,
124-
Addresses: addresses,
134+
Prefix: 24,
135+
Gateway: gateway,
136+
Addresses: addresses,
137+
AllocateReservedIPAddresses: true,
125138
}
126139

127140
switch poolType {
@@ -164,6 +177,7 @@ var _ = Describe("IP Pool Reconciler", func() {
164177
HaveField("Status.Addresses.Used", Equal(numClaims)))
165178

166179
genericPool.PoolSpec().Addresses = updatedAddresses
180+
genericPool.PoolSpec().AllocateReservedIPAddresses = false
167181
Expect(k8sClient.Update(context.Background(), genericPool)).To(Succeed())
168182

169183
Eventually(Object(genericPool)).
@@ -173,8 +187,20 @@ var _ = Describe("IP Pool Reconciler", func() {
173187

174188
Entry("InClusterIPPool",
175189
"InClusterIPPool", []string{"10.0.0.10-10.0.0.20"}, "10.0.0.1", []string{"10.0.0.13-10.0.0.20"}, 5, 3),
190+
Entry("InClusterIPPool when removing network address",
191+
"InClusterIPPool", []string{"10.0.0.0-10.0.0.255"}, "10.0.0.1", []string{"10.0.0.1-10.0.0.255"}, 4, 0),
192+
Entry("InClusterIPPool when removing gateway address",
193+
"InClusterIPPool", []string{"10.0.0.0-10.0.0.255"}, "10.0.0.1", []string{"10.0.0.0", "10.0.0.2-10.0.0.255"}, 5, 1),
194+
Entry("InClusterIPPool when removing broadcast address",
195+
"InClusterIPPool", []string{"10.0.0.251-10.0.0.255"}, "10.0.0.1", []string{"10.0.0.251-10.0.0.254"}, 5, 1),
176196
Entry("GlobalInClusterIPPool",
177197
"GlobalInClusterIPPool", []string{"10.0.0.10-10.0.0.20"}, "10.0.0.1", []string{"10.0.0.13-10.0.0.20"}, 5, 3),
198+
Entry("GlobalInClusterIPPool when removing network address",
199+
"GlobalInClusterIPPool", []string{"10.0.0.0-10.0.0.255"}, "10.0.0.1", []string{"10.0.0.1-10.0.0.255"}, 4, 0),
200+
Entry("GlobalInClusterIPPool when removing gateway address",
201+
"GlobalInClusterIPPool", []string{"10.0.0.0-10.0.0.255"}, "10.0.0.1", []string{"10.0.0.0", "10.0.0.2-10.0.0.255"}, 5, 1),
202+
Entry("GlobalInClusterIPPool when removing broadcast address",
203+
"GlobalInClusterIPPool", []string{"10.0.0.251-10.0.0.255"}, "10.0.0.1", []string{"10.0.0.251-10.0.0.254"}, 5, 1),
178204
)
179205
})
180206

internal/controllers/ipaddressclaim.go

+48-42
Original file line numberDiff line numberDiff line change
@@ -211,57 +211,60 @@ func (r *IPAddressClaimReconciler) Reconcile(ctx context.Context, req ctrl.Reque
211211
}
212212
}()
213213

214-
var pool pooltypes.GenericInClusterPool
214+
if !controllerutil.ContainsFinalizer(claim, ReleaseAddressFinalizer) {
215+
controllerutil.AddFinalizer(claim, ReleaseAddressFinalizer)
216+
}
215217

216-
if claim.Spec.PoolRef.Kind == inClusterIPPoolKind {
217-
icippool := &v1alpha2.InClusterIPPool{}
218-
if err := r.Client.Get(ctx, types.NamespacedName{Namespace: claim.Namespace, Name: claim.Spec.PoolRef.Name}, icippool); err != nil && !apierrors.IsNotFound(err) {
219-
return ctrl.Result{}, errors.Wrap(err, "failed to fetch pool")
220-
}
221-
pool = icippool
222-
} else if claim.Spec.PoolRef.Kind == globalInClusterIPPoolKind {
223-
gicippool := &v1alpha2.GlobalInClusterIPPool{}
224-
if err := r.Client.Get(ctx, types.NamespacedName{Name: claim.Spec.PoolRef.Name}, gicippool); err != nil && !apierrors.IsNotFound(err) {
225-
return ctrl.Result{}, errors.Wrap(err, "failed to fetch pool")
218+
pool, err := r.findPool(ctx, claim)
219+
if err != nil {
220+
if apierrors.IsNotFound(err) {
221+
err := errors.New("pool not found")
222+
log.Error(err, "the referenced pool could not be found")
223+
if !claim.ObjectMeta.DeletionTimestamp.IsZero() {
224+
return r.reconcileDelete(ctx, claim)
225+
}
226+
return ctrl.Result{}, nil
226227
}
227-
pool = gicippool
228+
return ctrl.Result{}, errors.Wrap(err, "failed to fetch pool")
228229
}
229230

230231
if pool != nil && annotations.HasPaused(pool) {
231232
log.Info("IPAddressClaim references Pool which is paused, skipping reconciliation.", "IPAddressClaim", claim.GetName(), "Pool", pool.GetName())
232233
return ctrl.Result{}, nil
233234
}
234235

235-
address := &ipamv1.IPAddress{}
236-
if err := r.Client.Get(ctx, req.NamespacedName, address); err != nil && !apierrors.IsNotFound(err) {
237-
return ctrl.Result{}, errors.Wrap(err, "failed to fetch address")
236+
if !claim.ObjectMeta.DeletionTimestamp.IsZero() {
237+
return r.reconcileDelete(ctx, claim)
238238
}
239239

240-
if !controllerutil.ContainsFinalizer(claim, ReleaseAddressFinalizer) {
241-
controllerutil.AddFinalizer(claim, ReleaseAddressFinalizer)
242-
}
240+
return r.reconcileNormal(ctx, claim, pool)
241+
}
243242

244-
if !claim.ObjectMeta.DeletionTimestamp.IsZero() {
245-
return r.reconcileDelete(ctx, claim, address)
243+
func (r *IPAddressClaimReconciler) findPool(ctx context.Context, claim *ipamv1.IPAddressClaim) (pooltypes.GenericInClusterPool, error) {
244+
if claim.Spec.PoolRef.Kind == inClusterIPPoolKind {
245+
icippool := &v1alpha2.InClusterIPPool{}
246+
if err := r.Client.Get(ctx, types.NamespacedName{Namespace: claim.Namespace, Name: claim.Spec.PoolRef.Name}, icippool); err != nil {
247+
return nil, err
248+
}
249+
return icippool, nil
250+
} else if claim.Spec.PoolRef.Kind == globalInClusterIPPoolKind {
251+
gicippool := &v1alpha2.GlobalInClusterIPPool{}
252+
if err := r.Client.Get(ctx, types.NamespacedName{Name: claim.Spec.PoolRef.Name}, gicippool); err != nil {
253+
return nil, err
254+
}
255+
return gicippool, nil
246256
}
257+
return nil, fmt.Errorf("unknown pool type: %s", claim.Spec.PoolRef.Kind)
258+
}
247259

248-
if pool == nil {
249-
err := errors.New("pool not found")
250-
log.Error(err, "the referenced pool could not be found")
251-
return ctrl.Result{}, nil
252-
}
260+
func (r *IPAddressClaimReconciler) reconcileNormal(ctx context.Context, claim *ipamv1.IPAddressClaim, pool pooltypes.GenericInClusterPool) (ctrl.Result, error) {
261+
log := ctrl.LoggerFrom(ctx).WithValues(pool.GetObjectKind().GroupVersionKind().Kind, fmt.Sprintf("%s/%s", pool.GetNamespace(), pool.GetName()))
253262

254263
addressesInUse, err := poolutil.ListAddressesInUse(ctx, r.Client, pool.GetNamespace(), claim.Spec.PoolRef)
255264
if err != nil {
256265
return ctrl.Result{}, fmt.Errorf("failed to list addresses: %w", err)
257266
}
258267

259-
return r.reconcile(ctx, claim, pool, addressesInUse)
260-
}
261-
262-
func (r *IPAddressClaimReconciler) reconcile(ctx context.Context, claim *ipamv1.IPAddressClaim, pool pooltypes.GenericInClusterPool, addressesInUse []ipamv1.IPAddress) (ctrl.Result, error) {
263-
log := ctrl.LoggerFrom(ctx).WithValues(pool.GetObjectKind().GroupVersionKind().Kind, fmt.Sprintf("%s/%s", pool.GetNamespace(), pool.GetName()))
264-
265268
address := poolutil.AddressByNamespacedName(addressesInUse, claim.Namespace, claim.Name)
266269
if address == nil {
267270
var err error
@@ -298,7 +301,16 @@ func (r *IPAddressClaimReconciler) reconcile(ctx context.Context, claim *ipamv1.
298301
return ctrl.Result{}, nil
299302
}
300303

301-
func (r *IPAddressClaimReconciler) reconcileDelete(ctx context.Context, claim *ipamv1.IPAddressClaim, address *ipamv1.IPAddress) (ctrl.Result, error) {
304+
func (r *IPAddressClaimReconciler) reconcileDelete(ctx context.Context, claim *ipamv1.IPAddressClaim) (ctrl.Result, error) {
305+
address := &ipamv1.IPAddress{}
306+
namespacedName := types.NamespacedName{
307+
Namespace: claim.Namespace,
308+
Name: claim.Name,
309+
}
310+
if err := r.Client.Get(ctx, namespacedName, address); err != nil && !apierrors.IsNotFound(err) {
311+
return ctrl.Result{}, errors.Wrap(err, "failed to fetch address")
312+
}
313+
302314
if address.Name != "" {
303315
var err error
304316
if controllerutil.RemoveFinalizer(address, ProtectAddressFinalizer) {
@@ -321,12 +333,12 @@ func (r *IPAddressClaimReconciler) reconcileDelete(ctx context.Context, claim *i
321333
func (r *IPAddressClaimReconciler) allocateAddress(claim *ipamv1.IPAddressClaim, pool pooltypes.GenericInClusterPool, addressesInUse []ipamv1.IPAddress) (*ipamv1.IPAddress, error) {
322334
poolSpec := pool.PoolSpec()
323335

324-
inUseIPSet, err := poolutil.AddressesToIPSet(buildAddressList(addressesInUse, poolSpec.Gateway))
336+
inUseIPSet, err := poolutil.AddressesToIPSet(buildAddressList(addressesInUse))
325337
if err != nil {
326338
return nil, fmt.Errorf("failed to convert IPAddressList to IPSet: %w", err)
327339
}
328340

329-
poolIPSet, err := poolutil.AddressesToIPSet(pool.PoolSpec().Addresses)
341+
poolIPSet, err := poolutil.PoolSpecToIPSet(poolSpec)
330342
if err != nil {
331343
return nil, fmt.Errorf("failed to convert pool to range: %w", err)
332344
}
@@ -367,17 +379,11 @@ func (r *IPAddressClaimReconciler) clusterToIPClaims(a client.Object) []reconcil
367379
return requests
368380
}
369381

370-
func buildAddressList(addressesInUse []ipamv1.IPAddress, gateway string) []string {
371-
// Add extra capacity for the case that the pool's gateway is specified
372-
addrStrings := make([]string, len(addressesInUse), len(addressesInUse)+1)
382+
func buildAddressList(addressesInUse []ipamv1.IPAddress) []string {
383+
addrStrings := make([]string, len(addressesInUse))
373384
for i, address := range addressesInUse {
374385
addrStrings[i] = address.Spec.Address
375386
}
376-
377-
if gateway != "" {
378-
addrStrings = append(addrStrings, gateway)
379-
}
380-
381387
return addrStrings
382388
}
383389

0 commit comments

Comments
 (0)