forked from projectcalico/calico
/
ippool.go
571 lines (495 loc) · 18.8 KB
/
ippool.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
// Copyright (c) 2017-2021 Tigera, Inc. All rights reserved.
// 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 clientv3
import (
"context"
"fmt"
"net"
"time"
log "github.com/sirupsen/logrus"
apiv3 "github.com/projectcalico/api/pkg/apis/projectcalico/v3"
"github.com/dtest11/calico/libcalico-go/lib/backend/model"
cerrors "github.com/dtest11/calico/libcalico-go/lib/errors"
cnet "github.com/dtest11/calico/libcalico-go/lib/net"
"github.com/dtest11/calico/libcalico-go/lib/options"
validator "github.com/dtest11/calico/libcalico-go/lib/validator/v3"
"github.com/dtest11/calico/libcalico-go/lib/watch"
)
// IPPoolInterface has methods to work with IPPool resources.
type IPPoolInterface interface {
Create(ctx context.Context, res *apiv3.IPPool, opts options.SetOptions) (*apiv3.IPPool, error)
Update(ctx context.Context, res *apiv3.IPPool, opts options.SetOptions) (*apiv3.IPPool, error)
Delete(ctx context.Context, name string, opts options.DeleteOptions) (*apiv3.IPPool, error)
Get(ctx context.Context, name string, opts options.GetOptions) (*apiv3.IPPool, error)
List(ctx context.Context, opts options.ListOptions) (*apiv3.IPPoolList, error)
Watch(ctx context.Context, opts options.ListOptions) (watch.Interface, error)
UnsafeCreate(ctx context.Context, res *apiv3.IPPool, opts options.SetOptions) (*apiv3.IPPool, error)
UnsafeDelete(ctx context.Context, name string, opts options.DeleteOptions) (*apiv3.IPPool, error)
}
// ipPools implements IPPoolInterface
type ipPools struct {
client client
}
// Create takes the representation of an IPPool and creates it. Returns the stored
// representation of the IPPool, and an error, if there is any.
func (r ipPools) Create(ctx context.Context, res *apiv3.IPPool, opts options.SetOptions) (*apiv3.IPPool, error) {
if res != nil {
// Since we're about to default some fields, take a (shallow) copy of the input data
// before we do so.
resCopy := *res
res = &resCopy
}
// Validate the IPPool before creating the resource.
if err := r.validateAndSetDefaults(ctx, res, nil, false); err != nil {
return nil, err
}
if err := validator.Validate(res); err != nil {
return nil, err
}
// Check that there are no existing blocks in the pool range that have a different block size.
poolBlockSize := res.Spec.BlockSize
poolIP, poolCIDR, err := net.ParseCIDR(res.Spec.CIDR)
if err != nil {
return nil, cerrors.ErrorParsingDatastoreEntry{
RawKey: "CIDR",
RawValue: string(res.Spec.CIDR),
Err: err,
}
}
ipVersion := 4
if poolIP.To4() == nil {
ipVersion = 6
}
blocks, err := r.client.backend.List(ctx, model.BlockListOptions{IPVersion: ipVersion}, "")
if _, ok := err.(cerrors.ErrorOperationNotSupported); !ok && err != nil {
// There was an error and it wasn't OperationNotSupported - return it.
return nil, err
} else if err == nil {
// Skip the block check if the error is OperationUnsupported - listing blocks is not
// supported with host-local IPAM on KDD.
for _, b := range blocks.KVPairs {
k := b.Key.(model.BlockKey)
ones, _ := k.CIDR.Mask.Size()
// Check if this block has a different size to the pool, and that it overlaps with the pool.
if ones != poolBlockSize && k.CIDR.IsNetOverlap(*poolCIDR) {
return nil, cerrors.ErrorValidation{
ErroredFields: []cerrors.ErroredField{{
Name: "IPPool.Spec.BlockSize",
Reason: "IPPool blocksSize conflicts with existing allocations that use a different blockSize",
Value: res.Spec.BlockSize,
}},
}
}
}
}
out, err := r.client.resources.Create(ctx, opts, apiv3.KindIPPool, res)
if out != nil {
return out.(*apiv3.IPPool), err
}
return nil, err
}
// Update takes the representation of an IPPool and updates it. Returns the stored
// representation of the IPPool, and an error, if there is any.
func (r ipPools) Update(ctx context.Context, res *apiv3.IPPool, opts options.SetOptions) (*apiv3.IPPool, error) {
if res != nil {
// Since we're about to default some fields, take a (shallow) copy of the input data
// before we do so.
resCopy := *res
res = &resCopy
}
// Get the existing settings, so that we can validate the CIDR and block size have not changed.
old, err := r.Get(ctx, res.Name, options.GetOptions{})
if err != nil {
return nil, err
}
// Validate the IPPool updating the resource.
if err := r.validateAndSetDefaults(ctx, res, old, false); err != nil {
return nil, err
}
if err := validator.Validate(res); err != nil {
return nil, err
}
out, err := r.client.resources.Update(ctx, opts, apiv3.KindIPPool, res)
if out != nil {
return out.(*apiv3.IPPool), err
}
return nil, err
}
// Delete takes name of the IPPool and deletes it. Returns an error if one occurs.
func (r ipPools) Delete(ctx context.Context, name string, opts options.DeleteOptions) (*apiv3.IPPool, error) {
// Deleting a pool requires a little care because of existing endpoints
// using IP addresses allocated in the pool. We do the deletion in
// the following steps:
// - disable the pool so no more IPs are assigned from it
// - remove all affinities associated with the pool
// - delete the pool
// Get the pool so that we can find the CIDR associated with it.
pool, err := r.Get(ctx, name, options.GetOptions{})
if err != nil {
return nil, err
}
logCxt := log.WithFields(log.Fields{
"CIDR": pool.Spec.CIDR,
"Name": name,
})
// If the pool is active, set the disabled flag to ensure we stop allocating from this pool.
if !pool.Spec.Disabled {
logCxt.Debug("Disabling pool to release affinities")
pool.Spec.Disabled = true
// If the Delete has been called with a ResourceVersion then use that to perform the
// update - that way we'll catch update conflicts (we could actually check here, but
// the most likely scenario is there isn't one - so just pass it through and let the
// Update handle any conflicts).
if opts.ResourceVersion != "" {
pool.ResourceVersion = opts.ResourceVersion
}
if _, err := r.Update(ctx, pool, options.SetOptions{}); err != nil {
return nil, err
}
// Reset the resource version before the actual delete since the version of that resource
// will now have been updated.
opts.ResourceVersion = ""
}
// Release affinities associated with this pool. We do this even if the pool was disabled
// (since it may have been enabled at one time, and if there are no affine blocks created
// then this will be a no-op). We've already validated the CIDR so we know it will parse.
if _, cidrNet, err := cnet.ParseCIDR(pool.Spec.CIDR); err == nil {
logCxt.Debug("Releasing pool affinities")
// Pause for a short period before releasing the affinities - this gives any in-progress
// allocations an opportunity to finish.
time.Sleep(500 * time.Millisecond)
err = r.client.IPAM().ReleasePoolAffinities(ctx, *cidrNet)
// Depending on the datastore, IPAM may not be supported. If we get a not supported
// error, then continue. Any other error, fail.
if _, ok := err.(cerrors.ErrorOperationNotSupported); !ok && err != nil {
return nil, err
}
}
// And finally, delete the pool.
out, err := r.client.resources.Delete(ctx, opts, apiv3.KindIPPool, noNamespace, name)
if out != nil {
return out.(*apiv3.IPPool), err
}
return nil, err
}
// Get takes name of the IPPool, and returns the corresponding IPPool object,
// and an error if there is any.
func (r ipPools) Get(ctx context.Context, name string, opts options.GetOptions) (*apiv3.IPPool, error) {
out, err := r.client.resources.Get(ctx, opts, apiv3.KindIPPool, noNamespace, name)
if out != nil {
convertIpPoolFromStorage(out.(*apiv3.IPPool))
return out.(*apiv3.IPPool), err
}
return nil, err
}
// List returns the list of IPPool objects that match the supplied options.
func (r ipPools) List(ctx context.Context, opts options.ListOptions) (*apiv3.IPPoolList, error) {
res := &apiv3.IPPoolList{}
if err := r.client.resources.List(ctx, opts, apiv3.KindIPPool, apiv3.KindIPPoolList, res); err != nil {
return nil, err
}
// Default values when reading from backend.
for i := range res.Items {
convertIpPoolFromStorage(&res.Items[i])
}
return res, nil
}
// Default pool values when reading from storage
func convertIpPoolFromStorage(pool *apiv3.IPPool) error {
// Default the blockSize if it wasn't previously set
if pool.Spec.BlockSize == 0 {
// Get the IP address of the CIDR to find the IP version
ipAddr, _, err := cnet.ParseCIDR(pool.Spec.CIDR)
if err != nil {
return cerrors.ErrorValidation{
ErroredFields: []cerrors.ErroredField{{
Name: "IPPool.Spec.CIDR",
Reason: "IPPool CIDR must be a valid subnet",
Value: pool.Spec.CIDR,
}},
}
}
if ipAddr.Version() == 4 {
pool.Spec.BlockSize = 26
} else {
pool.Spec.BlockSize = 122
}
}
// Default allowed uses if not set.
if len(pool.Spec.AllowedUses) == 0 {
pool.Spec.AllowedUses = []apiv3.IPPoolAllowedUse{apiv3.IPPoolAllowedUseWorkload, apiv3.IPPoolAllowedUseTunnel}
}
// Default the nodeSelector if it wasn't previously set.
if pool.Spec.NodeSelector == "" {
pool.Spec.NodeSelector = "all()"
}
// Default IPIPMode to "Never" if not set.
if len(pool.Spec.IPIPMode) == 0 {
pool.Spec.IPIPMode = apiv3.IPIPModeNever
}
// Default VXLANMode to "Never" if not set.
if len(pool.Spec.VXLANMode) == 0 {
pool.Spec.VXLANMode = apiv3.VXLANModeNever
}
return nil
}
// Watch returns a watch.Interface that watches the IPPools that match the
// supplied options.
func (r ipPools) Watch(ctx context.Context, opts options.ListOptions) (watch.Interface, error) {
return r.client.resources.Watch(ctx, opts, apiv3.KindIPPool, nil)
}
// validateAndSetDefaults validates IPPool fields and sets default values that are
// not assigned.
// The old pool will be unassigned for a Create.
func (r ipPools) validateAndSetDefaults(ctx context.Context, new, old *apiv3.IPPool, skipCIDROverlap bool) error {
errFields := []cerrors.ErroredField{}
// Spec.CIDR field must not be empty.
if new.Spec.CIDR == "" {
return cerrors.ErrorValidation{
ErroredFields: []cerrors.ErroredField{{
Name: "IPPool.Spec.CIDR",
Reason: "IPPool CIDR must be specified",
}},
}
}
// Make sure the CIDR is parsable.
ipAddr, cidr, err := cnet.ParseCIDR(new.Spec.CIDR)
if err != nil {
return cerrors.ErrorValidation{
ErroredFields: []cerrors.ErroredField{{
Name: "IPPool.Spec.CIDR",
Reason: "IPPool CIDR must be a valid subnet",
Value: new.Spec.CIDR,
}},
}
}
// Normalize the CIDR before persisting.
new.Spec.CIDR = cidr.String()
// If a nodeSelector is not specified, then this IP pool selects all nodes.
if new.Spec.NodeSelector == "" {
new.Spec.NodeSelector = "all()"
}
// If there was a previous pool then this must be an Update, validate that the
// CIDR has not changed. Since we are using normalized CIDRs we can just do a
// simple string comparison.
if old != nil && old.Spec.CIDR != new.Spec.CIDR {
errFields = append(errFields, cerrors.ErroredField{
Name: "IPPool.Spec.CIDR",
Reason: "IPPool CIDR cannot be modified",
Value: new.Spec.CIDR,
})
}
// Default the blockSize
if new.Spec.BlockSize == 0 {
if ipAddr.Version() == 4 {
new.Spec.BlockSize = 26
} else {
new.Spec.BlockSize = 122
}
}
// Default allowed uses if not set.
if len(new.Spec.AllowedUses) == 0 {
new.Spec.AllowedUses = []apiv3.IPPoolAllowedUse{apiv3.IPPoolAllowedUseWorkload, apiv3.IPPoolAllowedUseTunnel}
}
// Check that the blockSize hasn't changed since updates are not supported.
if old != nil && old.Spec.BlockSize != new.Spec.BlockSize {
errFields = append(errFields, cerrors.ErroredField{
Name: "IPPool.Spec.BlockSize",
Reason: "IPPool BlockSize cannot be modified",
Value: new.Spec.BlockSize,
})
}
if ipAddr.Version() == 4 {
if new.Spec.BlockSize > 32 || new.Spec.BlockSize < 20 {
errFields = append(errFields, cerrors.ErroredField{
Name: "IPPool.Spec.BlockSize",
Reason: "IPv4 block size must be between 20 and 32",
Value: new.Spec.BlockSize,
})
}
} else {
if new.Spec.BlockSize > 128 || new.Spec.BlockSize < 116 {
errFields = append(errFields, cerrors.ErroredField{
Name: "IPPool.Spec.BlockSize",
Reason: "IPv6 block size must be between 116 and 128",
Value: new.Spec.BlockSize,
})
}
}
// The Calico IPAM places restrictions on the minimum IP pool size. If
// the ippool is enabled, check that the pool is at least the minimum size.
if !new.Spec.Disabled {
ones, _ := cidr.Mask.Size()
log.Debugf("Pool CIDR: %s, mask: %d, blockSize: %d", cidr.String(), ones, new.Spec.BlockSize)
if ones > new.Spec.BlockSize {
errFields = append(errFields, cerrors.ErroredField{
Name: "IPPool.Spec.CIDR",
Reason: "IP pool size is too small for use with Calico IPAM. It must be equal to or greater than the block size.",
Value: new.Spec.CIDR,
})
}
}
// If there was no previous pool then this must be a Create. Check that the CIDR
// does not overlap with any other pool CIDRs.
if old == nil && !skipCIDROverlap {
allPools, err := r.List(ctx, options.ListOptions{})
if err != nil {
return err
}
for _, otherPool := range allPools.Items {
// It's possible that Create is called for a pre-existing pool, so skip our own
// pool and let the generic processing handle the pre-existing resource error case.
if otherPool.Name == new.Name {
continue
}
_, otherCIDR, err := cnet.ParseCIDR(otherPool.Spec.CIDR)
if err != nil {
log.WithField("Name", otherPool.Name).WithError(err).Error("IPPool is configured with an invalid CIDR")
continue
}
if otherCIDR.IsNetOverlap(cidr.IPNet) {
errFields = append(errFields, cerrors.ErroredField{
Name: "IPPool.Spec.CIDR",
Reason: fmt.Sprintf("IPPool(%s) CIDR overlaps with IPPool(%s) CIDR %s", new.Name, otherPool.Name, otherPool.Spec.CIDR),
Value: new.Spec.CIDR,
})
}
}
}
// Make sure IPIPMode is defaulted to "Never".
if len(new.Spec.IPIPMode) == 0 {
new.Spec.IPIPMode = apiv3.IPIPModeNever
}
// Make sure VXLANMode is defaulted to "Never".
if len(new.Spec.VXLANMode) == 0 {
new.Spec.VXLANMode = apiv3.VXLANModeNever
}
// Make sure only one of VXLAN and IPIP is enabled.
if new.Spec.VXLANMode != apiv3.VXLANModeNever && new.Spec.IPIPMode != apiv3.IPIPModeNever {
errFields = append(errFields, cerrors.ErroredField{
Name: "IPPool.Spec.VXLANMode",
Reason: "Cannot enable both VXLAN and IPIP on the same IPPool",
Value: new.Spec.VXLANMode,
})
}
// IPIP cannot be enabled for IPv6.
if cidr.Version() == 6 && new.Spec.IPIPMode != apiv3.IPIPModeNever {
errFields = append(errFields, cerrors.ErroredField{
Name: "IPPool.Spec.IPIPMode",
Reason: "IPIP is not supported on an IPv6 IP pool",
Value: new.Spec.IPIPMode,
})
}
// The Calico CIDR should be strictly masked
log.Debugf("IPPool CIDR: %s, Masked IP: %d", new.Spec.CIDR, cidr.IP)
if cidr.IP.String() != ipAddr.String() {
errFields = append(errFields, cerrors.ErroredField{
Name: "IPPool.Spec.CIDR",
Reason: "IPPool CIDR is not strictly masked",
Value: new.Spec.CIDR,
})
}
// IPv4 link local subnet.
ipv4LinkLocalNet := net.IPNet{
IP: net.ParseIP("169.254.0.0"),
Mask: net.CIDRMask(16, 32),
}
// IPv6 link local subnet.
ipv6LinkLocalNet := net.IPNet{
IP: net.ParseIP("fe80::"),
Mask: net.CIDRMask(10, 128),
}
// IP Pool CIDR cannot overlap with IPv4 or IPv6 link local address range.
if cidr.Version() == 4 && cidr.IsNetOverlap(ipv4LinkLocalNet) {
errFields = append(errFields, cerrors.ErroredField{
Name: "IPPool.Spec.CIDR",
Reason: "IPPool CIDR overlaps with IPv4 Link Local range 169.254.0.0/16",
Value: new.Spec.CIDR,
})
}
if cidr.Version() == 6 && cidr.IsNetOverlap(ipv6LinkLocalNet) {
errFields = append(errFields, cerrors.ErroredField{
Name: "IPPool.Spec.CIDR",
Reason: "IPPool CIDR overlaps with IPv6 Link Local range fe80::/10",
Value: new.Spec.CIDR,
})
}
// Return the errors if we have one or more validation errors.
if len(errFields) > 0 {
return cerrors.ErrorValidation{
ErroredFields: errFields,
}
}
return nil
}
// UnsafeCreate takes the representation of an IPPool and creates it the same as Create.
// It is unsafe because it will skip checks against overlapping blocks. This is only
// used to create child pools during operations to split IP pools.
func (r ipPools) UnsafeCreate(ctx context.Context, res *apiv3.IPPool, opts options.SetOptions) (*apiv3.IPPool, error) {
if res != nil {
// Since we're about to default some fields, take a (shallow) copy of the input data
// before we do so.
resCopy := *res
res = &resCopy
}
// Validate the IPPool before creating the resource.
if err := r.validateAndSetDefaults(ctx, res, nil, true); err != nil {
return nil, err
}
if err := validator.Validate(res); err != nil {
return nil, err
}
out, err := r.client.resources.Create(ctx, opts, apiv3.KindIPPool, res)
if out != nil {
return out.(*apiv3.IPPool), err
}
return nil, err
}
// UnsafeDelete deletes the IP pool similar to Delete except it does not delete the associated
// block affinities. This should only be used to remove an old IP pool after the split IP pools
// operations are complete.
func (r ipPools) UnsafeDelete(ctx context.Context, name string, opts options.DeleteOptions) (*apiv3.IPPool, error) {
// Get the pool so that we can find the CIDR associated with it.
pool, err := r.Get(ctx, name, options.GetOptions{})
if err != nil {
return nil, err
}
logCxt := log.WithFields(log.Fields{
"CIDR": pool.Spec.CIDR,
"Name": name,
})
// If the pool is active, set the disabled flag to ensure we stop allocating from this pool.
if !pool.Spec.Disabled {
logCxt.Debug("Disabling pool to release affinities")
pool.Spec.Disabled = true
// If the Delete has been called with a ResourceVersion then use that to perform the
// update - that way we'll catch update conflicts (we could actually check here, but
// the most likely scenario is there isn't one - so just pass it through and let the
// Update handle any conflicts).
if opts.ResourceVersion != "" {
pool.ResourceVersion = opts.ResourceVersion
}
if _, err := r.Update(ctx, pool, options.SetOptions{}); err != nil {
return nil, err
}
// Reset the resource version before the actual delete since the version of that resource
// will now have been updated.
opts.ResourceVersion = ""
}
// And finally, delete the pool.
out, err := r.client.resources.Delete(ctx, opts, apiv3.KindIPPool, noNamespace, name)
if out != nil {
return out.(*apiv3.IPPool), err
}
return nil, err
}