Browse Source

Add IPPool CR

This converts the ipammer to use an IPPool CR-based to persist IPAM state.

This followon patchset adds the actual persistence to the API Server:
https://review.opendev.org/c/airship/vino/+/774184

Change-Id: Ib81a0bc6be1c74d85e2adc2dcadd09590a65b292
changes/18/772118/8
Matt McEuen 2 years ago
parent
commit
40d4804ad1
  1. 3
      PROJECT
  2. 79
      config/crd/bases/airship.airshipit.org_ippools.yaml
  3. 3
      config/crd/kustomization.yaml
  4. 8
      config/crd/patches/cainjection_in_ippools.yaml
  5. 17
      config/crd/patches/webhook_in_ippools.yaml
  6. 24
      config/rbac/ippool_editor_role.yaml
  7. 20
      config/rbac/ippool_viewer_role.yaml
  8. 15
      config/samples/ippool.yaml
  9. 195
      docs/api/vino.md
  10. 68
      pkg/api/v1/ippool_types.go
  11. 114
      pkg/api/v1/zz_generated.deepcopy.go
  12. 18
      pkg/ipam/errors.go
  13. 51
      pkg/ipam/ipam.go
  14. 111
      pkg/ipam/ipam_test.go
  15. 1
      tools/deployment/test-cr.sh

3
PROJECT

@ -4,4 +4,7 @@ resources:
- group: airship
kind: Vino
version: v1
- group: airship
kind: IPPool
version: v1
version: "2"

79
config/crd/bases/airship.airshipit.org_ippools.yaml

@ -0,0 +1,79 @@
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.3.0
creationTimestamp: null
name: ippools.airship.airshipit.org
spec:
group: airship.airshipit.org
names:
kind: IPPool
listKind: IPPoolList
plural: ippools
singular: ippool
scope: Namespaced
validation:
openAPIV3Schema:
description: IPPool is the Schema for the ippools API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: IPPool tracks allocation ranges and statuses within a specific
subnet IPv4 or IPv6 subnet. It has a set of ranges of IPs within the
subnet from which IPs can be allocated by IPAM, and a set of IPs that
are currently allocated already.
properties:
allocatedIPs:
items:
type: string
type: array
ranges:
items:
description: Range has (inclusive) bounds within a subnet from which
IPs can be allocated
properties:
start:
type: string
stop:
type: string
required:
- start
- stop
type: object
type: array
subnet:
type: string
required:
- allocatedIPs
- ranges
- subnet
type: object
status:
description: IPPoolStatus defines the observed state of IPPool
type: object
type: object
version: v1
versions:
- name: v1
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

3
config/crd/kustomization.yaml

@ -3,6 +3,7 @@
# It should be run by config/default
resources:
- bases/airship.airshipit.org_vinoes.yaml
- bases/airship.airshipit.org_ippools.yaml
- bases/bmh.yaml
# +kubebuilder:scaffold:crdkustomizeresource
@ -10,11 +11,13 @@ patchesStrategicMerge:
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
# patches here are for enabling the conversion webhook for each CRD
#- patches/webhook_in_vinoes.yaml
#- patches/webhook_in_ippools.yaml
# +kubebuilder:scaffold:crdkustomizewebhookpatch
# [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix.
# patches here are for enabling the CA injection for each CRD
#- patches/cainjection_in_vinoes.yaml
#- patches/cainjection_in_ippools.yaml
# +kubebuilder:scaffold:crdkustomizecainjectionpatch
# the following config is for teaching kustomize how to do kustomization for CRDs.

8
config/crd/patches/cainjection_in_ippools.yaml

@ -0,0 +1,8 @@
# The following patch adds a directive for certmanager to inject CA into the CRD
# CRD conversion requires k8s 1.13 or later.
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
annotations:
cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
name: ippools.airship.airshipit.org

17
config/crd/patches/webhook_in_ippools.yaml

@ -0,0 +1,17 @@
# The following patch enables conversion webhook for CRD
# CRD conversion requires k8s 1.13 or later.
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: ippools.airship.airshipit.org
spec:
conversion:
strategy: Webhook
webhookClientConfig:
# this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank,
# but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager)
caBundle: Cg==
service:
namespace: system
name: webhook-service
path: /convert

24
config/rbac/ippool_editor_role.yaml

@ -0,0 +1,24 @@
# permissions for end users to edit ippools.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: ippool-editor-role
rules:
- apiGroups:
- airship.airshipit.org
resources:
- ippools
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- airship.airshipit.org
resources:
- ippools/status
verbs:
- get

20
config/rbac/ippool_viewer_role.yaml

@ -0,0 +1,20 @@
# permissions for end users to view ippools.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: ippool-viewer-role
rules:
- apiGroups:
- airship.airshipit.org
resources:
- ippools
verbs:
- get
- list
- watch
- apiGroups:
- airship.airshipit.org
resources:
- ippools/status
verbs:
- get

15
config/samples/ippool.yaml

@ -0,0 +1,15 @@
apiVersion: airship.airshipit.org/v1
kind: IPPool
metadata:
name: ippool-sample
spec:
subnet: 10.0.0.0/16
ranges:
- start: 10.0.0.1
stop: 10.0.0.9
- start: 10.0.1.1
stop: 10.0.1.9
allocatedIPs:
- 10.0.0.1
- 10.0.0.2
- 10.0.1.1

195
docs/api/vino.md

@ -218,6 +218,160 @@ bool
</table>
</div>
</div>
<h3 id="airship.airshipit.org/v1.IPPool">IPPool
</h3>
<p>IPPool is the Schema for the ippools API</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>metadata</code><br>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#objectmeta-v1-meta">
Kubernetes meta/v1.ObjectMeta
</a>
</em>
</td>
<td>
Refer to the Kubernetes API documentation for the fields of the
<code>metadata</code> field.
</td>
</tr>
<tr>
<td>
<code>spec</code><br>
<em>
<a href="#airship.airshipit.org/v1.IPPoolSpec">
IPPoolSpec
</a>
</em>
</td>
<td>
<br/>
<br/>
<table>
<tr>
<td>
<code>subnet</code><br>
<em>
string
</em>
</td>
<td>
</td>
</tr>
<tr>
<td>
<code>ranges</code><br>
<em>
<a href="#airship.airshipit.org/v1.Range">
[]Range
</a>
</em>
</td>
<td>
</td>
</tr>
<tr>
<td>
<code>allocatedIPs</code><br>
<em>
[]string
</em>
</td>
<td>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<code>status</code><br>
<em>
<a href="#airship.airshipit.org/v1.IPPoolStatus">
IPPoolStatus
</a>
</em>
</td>
<td>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="airship.airshipit.org/v1.IPPoolSpec">IPPoolSpec
</h3>
<p>
(<em>Appears on:</em>
<a href="#airship.airshipit.org/v1.IPPool">IPPool</a>)
</p>
<p>IPPool tracks allocation ranges and statuses within a specific
subnet IPv4 or IPv6 subnet. It has a set of ranges of IPs
within the subnet from which IPs can be allocated by IPAM,
and a set of IPs that are currently allocated already.</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>subnet</code><br>
<em>
string
</em>
</td>
<td>
</td>
</tr>
<tr>
<td>
<code>ranges</code><br>
<em>
<a href="#airship.airshipit.org/v1.Range">
[]Range
</a>
</em>
</td>
<td>
</td>
</tr>
<tr>
<td>
<code>allocatedIPs</code><br>
<em>
[]string
</em>
</td>
<td>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="airship.airshipit.org/v1.IPPoolStatus">IPPoolStatus
</h3>
<p>
(<em>Appears on:</em>
<a href="#airship.airshipit.org/v1.IPPool">IPPool</a>)
</p>
<p>IPPoolStatus defines the observed state of IPPool</p>
<h3 id="airship.airshipit.org/v1.NamespacedName">NamespacedName
</h3>
<p>
@ -549,6 +703,47 @@ DiskDrivesTemplate
</table>
</div>
</div>
<h3 id="airship.airshipit.org/v1.Range">Range
</h3>
<p>
(<em>Appears on:</em>
<a href="#airship.airshipit.org/v1.IPPoolSpec">IPPoolSpec</a>)
</p>
<p>Range has (inclusive) bounds within a subnet from which IPs can be allocated</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>start</code><br>
<em>
string
</em>
</td>
<td>
</td>
</tr>
<tr>
<td>
<code>stop</code><br>
<em>
string
</em>
</td>
<td>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="airship.airshipit.org/v1.VMNodeFlavor">VMNodeFlavor
</h3>
<p>

68
pkg/api/v1/ippool_types.go

@ -0,0 +1,68 @@
/*
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 v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// IPPool tracks allocation ranges and statuses within a specific
// subnet IPv4 or IPv6 subnet. It has a set of ranges of IPs
// within the subnet from which IPs can be allocated by IPAM,
// and a set of IPs that are currently allocated already.
type IPPoolSpec struct {
Subnet string `json:"subnet"`
Ranges []Range `json:"ranges"`
AllocatedIPs []string `json:"allocatedIPs"`
}
// Range has (inclusive) bounds within a subnet from which IPs can be allocated
type Range struct {
Start string `json:"start"`
Stop string `json:"stop"`
}
// IPPoolStatus defines the observed state of IPPool
type IPPoolStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
}
// +kubebuilder:object:root=true
// IPPool is the Schema for the ippools API
type IPPool struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec IPPoolSpec `json:"spec,omitempty"`
Status IPPoolStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// IPPoolList contains a list of IPPool
type IPPoolList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []IPPool `json:"items"`
}
func init() {
SchemeBuilder.Register(&IPPool{}, &IPPoolList{})
}

114
pkg/api/v1/zz_generated.deepcopy.go

@ -91,6 +91,105 @@ func (in *DiskOptions) DeepCopy() *DiskOptions {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IPPool) DeepCopyInto(out *IPPool) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
out.Status = in.Status
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPool.
func (in *IPPool) DeepCopy() *IPPool {
if in == nil {
return nil
}
out := new(IPPool)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *IPPool) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IPPoolList) DeepCopyInto(out *IPPoolList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]IPPool, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPoolList.
func (in *IPPoolList) DeepCopy() *IPPoolList {
if in == nil {
return nil
}
out := new(IPPoolList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *IPPoolList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IPPoolSpec) DeepCopyInto(out *IPPoolSpec) {
*out = *in
if in.Ranges != nil {
in, out := &in.Ranges, &out.Ranges
*out = make([]Range, len(*in))
copy(*out, *in)
}
if in.AllocatedIPs != nil {
in, out := &in.AllocatedIPs, &out.AllocatedIPs
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPoolSpec.
func (in *IPPoolSpec) DeepCopy() *IPPoolSpec {
if in == nil {
return nil
}
out := new(IPPoolSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IPPoolStatus) DeepCopyInto(out *IPPoolStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPPoolStatus.
func (in *IPPoolStatus) DeepCopy() *IPPoolStatus {
if in == nil {
return nil
}
out := new(IPPoolStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespacedName) DeepCopyInto(out *NamespacedName) {
*out = *in
@ -206,6 +305,21 @@ func (in *NodeSet) DeepCopy() *NodeSet {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Range) DeepCopyInto(out *Range) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Range.
func (in *Range) DeepCopy() *Range {
if in == nil {
return nil
}
out := new(Range)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VMNodeFlavor) DeepCopyInto(out *VMNodeFlavor) {
*out = *in

18
pkg/ipam/errors.go

@ -16,6 +16,8 @@ package ipam
import (
"fmt"
vinov1 "vino/pkg/api/v1"
)
// ErrSubnetNotAllocated returned if the subnet is not registered in IPAM
@ -23,23 +25,28 @@ type ErrSubnetNotAllocated struct {
Subnet string
}
// ErrSubnetRangeInvalid returned if a requested subnet's range is not valid
type ErrSubnetRangeInvalid struct {
SubnetRange vinov1.Range
}
// ErrSubnetRangeOverlapsWithExistingRange returned if the subnet's range
// overlaps (partially or completely) with an already added range in that subnet
type ErrSubnetRangeOverlapsWithExistingRange struct {
Subnet string
SubnetRange Range
SubnetRange vinov1.Range
}
// ErrSubnetRangeNotAllocated returned if the subnet's range is not registered in IPAM
type ErrSubnetRangeNotAllocated struct {
Subnet string
SubnetRange Range
SubnetRange vinov1.Range
}
// ErrSubnetRangeExhausted returned if the subnet's range has no unallocated IPs
type ErrSubnetRangeExhausted struct {
Subnet string
SubnetRange Range
SubnetRange vinov1.Range
}
// ErrInvalidIPAddress returned if an IP address string is malformed
@ -56,6 +63,11 @@ func (e ErrSubnetNotAllocated) Error() string {
return fmt.Sprintf("IPAM subnet %s not allocated", e.Subnet)
}
func (e ErrSubnetRangeInvalid) Error() string {
return fmt.Sprintf("IPAM range [%s,%s] is invalid",
e.SubnetRange.Start, e.SubnetRange.Stop)
}
func (e ErrSubnetRangeOverlapsWithExistingRange) Error() string {
return fmt.Sprintf("IPAM range [%s,%s] in subnet %s overlaps with an existing range",
e.SubnetRange.Start, e.SubnetRange.Stop, e.Subnet)

51
pkg/ipam/ipam.go

@ -19,6 +19,8 @@ import (
"strings"
"unsafe"
vinov1 "vino/pkg/api/v1"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
@ -30,45 +32,46 @@ type Ipam struct {
Scheme *runtime.Scheme
Client client.Client
ippools map[string]*IPPool
}
// IPPool tracks allocation ranges and statuses within a specific
// subnet IPv4 or IPv6 subnet. It has a set of ranges of IPs
// within the subnet from which IPs can be allocated by IPAM,
// and a set of IPs that are currently allocated already.
type IPPool struct {
Subnet string
Ranges []Range
AllocatedIPs []string
}
// Range has (inclusive) bounds within a subnet from which IPs can be allocated
type Range struct {
Start string
Stop string
ippools map[string]*vinov1.IPPoolSpec
}
// NewIpam initializes an empty IPAM configuration.
// TODO: persist and refresh state from the API server
// TODO: add ability to remove IP addresses and ranges
func NewIpam() *Ipam {
ippools := make(map[string]*IPPool)
ippools := make(map[string]*vinov1.IPPoolSpec)
return &Ipam{
ippools: ippools,
}
}
// Create a new Range, validating its input
func NewRange(start string, stop string) (vinov1.Range, error) {
r := vinov1.Range{Start: start, Stop: stop}
a, e := ipStringToInt(start)
if e != nil {
return vinov1.Range{}, e
}
b, e := ipStringToInt(stop)
if e != nil {
return vinov1.Range{}, e
}
if b < a {
return vinov1.Range{}, ErrSubnetRangeInvalid{r}
}
return r, nil
}
// AddSubnetRange adds a range within a subnet for IP allocation
// TODO error: invalid range for subnet
// TODO error: range overlaps with existing range or subnet overlaps with existing subnet
func (i *Ipam) AddSubnetRange(subnet string, subnetRange Range) error {
// TODO error: invalid range for subnet
func (i *Ipam) AddSubnetRange(subnet string, subnetRange vinov1.Range) error {
// Does the subnet already exist? (this is fine)
ippool, exists := i.ippools[subnet]
if !exists {
ippool = &IPPool{
ippool = &vinov1.IPPoolSpec{
Subnet: subnet,
Ranges: []Range{subnetRange}, // TODO DeepCopy()
Ranges: []vinov1.Range{subnetRange},
AllocatedIPs: []string{},
}
i.ippools[subnet] = ippool
@ -91,7 +94,7 @@ func (i *Ipam) AddSubnetRange(subnet string, subnetRange Range) error {
}
// AllocateIP allocates an IP from a range and return it
func (i *Ipam) AllocateIP(subnet string, subnetRange Range) (string, error) {
func (i *Ipam) AllocateIP(subnet string, subnetRange vinov1.Range) (string, error) {
// NOTE/TODO: this is not threadsafe, which is fine because
// the final impl will use the api server as the backend, not local.
ippool, exists := i.ippools[subnet]
@ -122,7 +125,7 @@ func (i *Ipam) AllocateIP(subnet string, subnetRange Range) (string, error) {
// steps through them looking for one that that is not already
// in use, converts it back to a string and returns it.
// It does not itself add it to the list of assigned IPs.
func findFreeIPInRange(ippool *IPPool, subnetRange Range) (string, error) {
func findFreeIPInRange(ippool *vinov1.IPPoolSpec, subnetRange vinov1.Range) (string, error) {
allocatedIPSet := sliceToMap(ippool.AllocatedIPs)
intToString := intToIPv4String
if strings.Contains(ippool.Subnet, ":") {

111
pkg/ipam/ipam_test.go

@ -18,6 +18,8 @@ import (
"math"
"testing"
vinov1 "vino/pkg/api/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -25,53 +27,53 @@ import (
func TestAllocateIP(t *testing.T) {
tests := []struct {
name, subnet, expectedErr string
subnetRange Range
subnetRange vinov1.Range
}{
{
name: "success ipv4",
subnet: "10.0.0.0/16",
subnetRange: Range{"10.0.1.0", "10.0.1.9"},
subnetRange: vinov1.Range{Start: "10.0.1.0", Stop: "10.0.1.9"},
},
{
name: "success ipv6",
subnet: "2600:1700:b030:0000::/72",
subnetRange: Range{"2600:1700:b030:0000::", "2600:1700:b030:0009::"},
subnetRange: vinov1.Range{Start: "2600:1700:b030:0000::", Stop: "2600:1700:b030:0009::"},
},
{
name: "error subnet not allocated ipv4",
subnet: "10.0.0.0/20",
subnetRange: Range{"10.0.1.0", "10.0.1.9"},
subnetRange: vinov1.Range{Start: "10.0.1.0", Stop: "10.0.1.9"},
expectedErr: "IPAM subnet 10.0.0.0/20 not allocated",
},
{
name: "error subnet not allocated ipv6",
subnet: "2600:1700:b030:0000::/80",
subnetRange: Range{"2600:1700:b030:0000::", "2600:1700:b030:0009::"},
subnetRange: vinov1.Range{Start: "2600:1700:b030:0000::", Stop: "2600:1700:b030:0009::"},
expectedErr: "IPAM subnet 2600:1700:b030:0000::/80 not allocated",
},
{
name: "error range not allocated ipv4",
subnet: "10.0.0.0/16",
subnetRange: Range{"10.0.2.0", "10.0.2.9"},
subnetRange: vinov1.Range{Start: "10.0.2.0", Stop: "10.0.2.9"},
expectedErr: "IPAM range [10.0.2.0,10.0.2.9] in subnet 10.0.0.0/16 is not allocated",
},
{
name: "error range not allocated ipv6",
subnet: "2600:1700:b030:0000::/72",
subnetRange: Range{"2600:1700:b030:0000::", "2600:1700:b030:1111::"},
subnetRange: vinov1.Range{Start: "2600:1700:b030:0000::", Stop: "2600:1700:b030:1111::"},
expectedErr: "IPAM range [2600:1700:b030:0000::,2600:1700:b030:1111::] " +
"in subnet 2600:1700:b030:0000::/72 is not allocated",
},
{
name: "error range exhausted ipv4",
subnet: "192.168.0.0/1",
subnetRange: Range{"192.168.0.0", "192.168.0.0"},
subnetRange: vinov1.Range{Start: "192.168.0.0", Stop: "192.168.0.0"},
expectedErr: "IPAM range [192.168.0.0,192.168.0.0] in subnet 192.168.0.0/1 is exhausted",
},
{
name: "error range exhausted ipv6",
subnet: "2600:1700:b031:0000::/64",
subnetRange: Range{"2600:1700:b031:0000::", "2600:1700:b031:0000::"},
subnetRange: vinov1.Range{Start: "2600:1700:b031:0000::", Stop: "2600:1700:b031:0000::"},
expectedErr: "IPAM range [2600:1700:b031:0000::,2600:1700:b031:0000::] " +
"in subnet 2600:1700:b031:0000::/64 is exhausted",
},
@ -83,17 +85,21 @@ func TestAllocateIP(t *testing.T) {
ipammer := NewIpam()
// Pre-populate IPAM with some precondition test data
err := ipammer.AddSubnetRange("10.0.0.0/16", Range{"10.0.1.0", "10.0.1.9"})
err := ipammer.AddSubnetRange("10.0.0.0/16", vinov1.Range{Start: "10.0.1.0", Stop: "10.0.1.9"})
require.NoError(t, err)
err = ipammer.AddSubnetRange("2600:1700:b030:0000::/72", Range{"2600:1700:b030:0000::", "2600:1700:b030:0009::"})
err = ipammer.AddSubnetRange("2600:1700:b030:0000::/72",
vinov1.Range{Start: "2600:1700:b030:0000::", Stop: "2600:1700:b030:0009::"})
require.NoError(t, err)
err = ipammer.AddSubnetRange("192.168.0.0/1", Range{"192.168.0.0", "192.168.0.0"})
err = ipammer.AddSubnetRange("192.168.0.0/1",
vinov1.Range{Start: "192.168.0.0", Stop: "192.168.0.0"})
require.NoError(t, err)
err = ipammer.AddSubnetRange("2600:1700:b031:0000::/64", Range{"2600:1700:b031:0000::", "2600:1700:b031:0000::"})
err = ipammer.AddSubnetRange("2600:1700:b031:0000::/64",
vinov1.Range{Start: "2600:1700:b031:0000::", Stop: "2600:1700:b031:0000::"})
require.NoError(t, err)
_, err = ipammer.AllocateIP("192.168.0.0/1", Range{"192.168.0.0", "192.168.0.0"})
_, err = ipammer.AllocateIP("192.168.0.0/1", vinov1.Range{Start: "192.168.0.0", Stop: "192.168.0.0"})
require.NoError(t, err)
_, err = ipammer.AllocateIP("2600:1700:b031:0000::/64", Range{"2600:1700:b031:0000::", "2600:1700:b031:0000::"})
_, err = ipammer.AllocateIP("2600:1700:b031:0000::/64",
vinov1.Range{Start: "2600:1700:b031:0000::", Stop: "2600:1700:b031:0000::"})
require.NoError(t, err)
ip, err := ipammer.AllocateIP(tt.subnet, tt.subnetRange)
if tt.expectedErr != "" {
@ -108,22 +114,67 @@ func TestAllocateIP(t *testing.T) {
}
}
func TestNewRange(t *testing.T) {
tests := []struct {
name, start, stop, expectedErr string
}{
{
name: "success",
start: "10.0.0.1",
stop: "10.0.0.2",
expectedErr: "",
},
{
name: "error stop less than start",
start: "10.0.0.2",
stop: "10.0.0.1",
expectedErr: "is invalid",
},
{
name: "error bad start",
start: "10.0.0.2.x",
stop: "10.0.0.1",
expectedErr: "is invalid",
},
{
name: "error bad stop",
start: "10.0.0.2",
stop: "10.0.0.1.x",
expectedErr: "is invalid",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
r, err := NewRange(tt.start, tt.stop)
if tt.expectedErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedErr)
} else {
require.NoError(t, err)
assert.Equal(t, r.Start, tt.start)
assert.Equal(t, r.Stop, tt.stop)
}
})
}
}
// Test some error handling that is not captured by TestAllocateIP
func TestAddSubnetRange(t *testing.T) {
tests := []struct {
name, subnet, expectedErr string
subnetRange Range
subnetRange vinov1.Range
}{
{
name: "success",
subnet: "10.0.0.0/16",
subnetRange: Range{"10.0.2.0", "10.0.2.9"},
subnetRange: vinov1.Range{Start: "10.0.2.0", Stop: "10.0.2.9"},
expectedErr: "",
},
{
name: "error range already exists",
subnet: "10.0.0.0/16",
subnetRange: Range{"10.0.1.0", "10.0.1.9"},
subnetRange: vinov1.Range{Start: "10.0.1.0", Stop: "10.0.1.9"},
expectedErr: "IPAM range [10.0.1.0,10.0.1.9] in subnet 10.0.0.0/16 overlaps",
},
// TODO: check for partially overlapping ranges and subnets
@ -135,7 +186,7 @@ func TestAddSubnetRange(t *testing.T) {
ipammer := NewIpam()
// Pre-populate IPAM with some precondition test data
err := ipammer.AddSubnetRange("10.0.0.0/16", Range{"10.0.1.0", "10.0.1.9"})
err := ipammer.AddSubnetRange("10.0.0.0/16", vinov1.Range{Start: "10.0.1.0", Stop: "10.0.1.9"})
require.NoError(t, err)
err = ipammer.AddSubnetRange(tt.subnet, tt.subnetRange)
if tt.expectedErr != "" {
@ -151,33 +202,33 @@ func TestFindFreeIPInRange(t *testing.T) {
tests := []struct {
name string
subnet string
subnetRange Range
subnetRange vinov1.Range
out string
expectedErr string
}{
{
name: "ip available IPv4",
subnet: "10.0.0.0/16",
subnetRange: Range{"10.0.1.0", "10.0.1.10"},
subnetRange: vinov1.Range{Start: "10.0.1.0", Stop: "10.0.1.10"},
out: "10.0.1.0",
},
{
name: "ip unavailable IPv4",
subnet: "10.0.0.0/16",
subnetRange: Range{"10.0.2.0", "10.0.2.0"},
subnetRange: vinov1.Range{Start: "10.0.2.0", Stop: "10.0.2.0"},
out: "",
expectedErr: "IPAM range [10.0.2.0,10.0.2.0] in subnet 10.0.0.0/16 is exhausted",
},
{
name: "ip available IPv6",
subnet: "2600:1700:b030:0000::/64",
subnetRange: Range{"2600:1700:b030:1001::", "2600:1700:b030:1009::"},
subnetRange: vinov1.Range{Start: "2600:1700:b030:1001::", Stop: "2600:1700:b030:1009::"},
out: "2600:1700:b030:1001::",
},
{
name: "ip unavailable IPv6",
subnet: "2600:1700:b031::/64",
subnetRange: Range{"2600:1700:b031::", "2600:1700:b031::"},
subnetRange: vinov1.Range{Start: "2600:1700:b031::", Stop: "2600:1700:b031::"},
expectedErr: "IPAM range [2600:1700:b031::,2600:1700:b031::] " +
"in subnet 2600:1700:b031::/64 is exhausted",
},
@ -186,14 +237,14 @@ func TestFindFreeIPInRange(t *testing.T) {
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
ippool := IPPool{
ippool := vinov1.IPPoolSpec{
Subnet: tt.subnet,
// One available and one unavailable range each for ipv4/6
Ranges: []Range{
{"10.0.1.0", "10.0.1.10"},
{"10.0.2.0", "10.0.2.0"},
{"2600:1700:b030:1001::", "2600:1700:b030:1009::"},
{"2600:1700:b031::", "2600:1700:b031::"},
Ranges: []vinov1.Range{
{Start: "10.0.1.0", Stop: "10.0.1.10"},
{Start: "10.0.2.0", Stop: "10.0.2.0"},
{Start: "2600:1700:b030:1001::", Stop: "2600:1700:b030:1009::"},
{Start: "2600:1700:b031::", Stop: "2600:1700:b031::"},
},
AllocatedIPs: []string{"10.0.2.0", "2600:1700:b031::"},
}

1
tools/deployment/test-cr.sh

@ -13,6 +13,7 @@ function vinoDebugInfo () {
}
kubectl apply -f config/samples/vino_cr.yaml
kubectl apply -f config/samples/ippool.yaml
# Remove logs collection from here, when we will have zuul collect logs job
until [[ $(kubectl get vino vino-test-cr 2>/dev/null) ]]; do

Loading…
Cancel
Save