Skip to content

Commit

Permalink
Allocate external ips in admission controller
Browse files Browse the repository at this point in the history
  • Loading branch information
marun committed Jul 6, 2016
1 parent 49f8d37 commit 6e194c2
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 2 deletions.
73 changes: 73 additions & 0 deletions pkg/service/admission/externalip_admission.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package admission

import (
"errors"
"fmt"
"io"
"net"
"strings"
Expand All @@ -10,6 +12,10 @@ import (
apierrs "k8s.io/kubernetes/pkg/api/errors"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/util/validation/field"

"github.com/openshift/origin/pkg/client"
oadmission "github.com/openshift/origin/pkg/cmd/server/admission"
externalipapi "github.com/openshift/origin/pkg/externalip/api"
)

const ExternalIPPluginName = "ExternalIPRanger"
Expand All @@ -22,12 +28,16 @@ func init() {

type externalIPRanger struct {
*kadmission.Handler
client client.Interface
reject []*net.IPNet
admit []*net.IPNet
}

var _ kadmission.Interface = &externalIPRanger{}

var _ = oadmission.WantsOpenshiftClient(&externalIPRanger{})
var _ = oadmission.Validator(&externalIPRanger{})

// ParseCIDRRules calculates a blacklist and whitelist from a list of string CIDR rules (treating
// a leading ! as a negation). Returns an error if any rule is invalid.
func ParseCIDRRules(rules []string) (reject, admit []*net.IPNet, err error) {
Expand Down Expand Up @@ -72,6 +82,19 @@ func (s NetworkSlice) Contains(ip net.IP) bool {
return false
}

func (r *externalIPRanger) SetOpenshiftClient(c client.Interface) {
r.client = c
}

func (r *externalIPRanger) Validate() error {
if r.client == nil {
return fmt.Errorf("ExternalIPRanger needs an Openshift client")
}
return nil
}

var externalIPInUse = errors.New("ExternalIP is in use")

// Admit determines if the service should be admitted based on the configured network CIDR.
func (r *externalIPRanger) Admit(a kadmission.Attributes) error {
if a.GetResource().GroupResource() != kapi.Resource("services") {
Expand Down Expand Up @@ -103,8 +126,58 @@ func (r *externalIPRanger) Admit(a kadmission.Attributes) error {
}
}
}

// Attempt to allocate the specified external ips
// TODO cleanup newly allocated ips on failure
if len(errs) == 0 {
for i, ip := range svc.Spec.ExternalIPs {
err := r.allocateExternalIP(ip, svc.Namespace, svc.Name)
if err != nil {
if err == externalIPInUse {
errs = append(errs, field.Duplicate(field.NewPath("spec", "externalIPs").Index(i), err.Error()))
} else {
errs = append(errs, field.InternalError(field.NewPath("spec", "externalIPs").Index(i), err))
}
// Skip allocating ips after an allocation failure
break
}
}
}

if len(errs) > 0 {
return apierrs.NewInvalid(a.GetKind().GroupKind(), a.GetName(), errs)
}
return nil
}

// allocateExternalIP will attempt to allocate the given ip address
// for the service identified by serviceNamespace and serviceName.
// The return value will be nil if allocation was successful, or an
// error otherwise.
func (r *externalIPRanger) allocateExternalIP(ip, serviceNamespace, serviceName string) error {
externalIP, err := r.client.ExternalIPs().Get(ip)
if apierrs.IsNotFound(err) {
externalIP = &externalipapi.ExternalIP{
ObjectMeta: kapi.ObjectMeta{
Name: ip,
},
ServiceRef: kapi.ObjectReference{
Kind: "Service",
Namespace: serviceNamespace,
Name: serviceName,
},
}
_, err := r.client.ExternalIPs().Create(externalIP)
if apierrs.IsAlreadyExists(err) {
return externalIPInUse
} else {
return err
}
} else if err == nil {
allocationExists := (externalIP.ServiceRef.Namespace == serviceNamespace && externalIP.ServiceRef.Name == serviceName)
if !allocationExists {
return externalIPInUse
}
}
return err
}
123 changes: 121 additions & 2 deletions pkg/service/admission/externalip_admission_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ import (

"k8s.io/kubernetes/pkg/admission"
kapi "k8s.io/kubernetes/pkg/api"
kapierrors "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/api/unversioned"
ktestclient "k8s.io/kubernetes/pkg/client/unversioned/testclient"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util"

client "github.com/openshift/origin/pkg/client/testclient"
externalipapi "github.com/openshift/origin/pkg/externalip/api"
)

// TestAdmission verifies various scenarios involving pod/project/global node label selectors
Expand Down Expand Up @@ -128,7 +137,16 @@ func TestAdmission(t *testing.T) {
op: admission.Update,
testName: "IP in range on update",
},

// Without a pre-existing allocation, the fake client will
// return an empty external ip object. This is useful for
// verifying the case of failure to allocate.
{
admit: false,
admits: []*net.IPNet{ipv4},
externalIPs: []string{"172.0.0.1"},
op: admission.Create,
testName: "IP could not be allocated",
},
// other checks
{
admit: false,
Expand Down Expand Up @@ -160,7 +178,16 @@ func TestAdmission(t *testing.T) {
for _, test := range tests {
svc.Spec.ExternalIPs = test.externalIPs
handler := NewExternalIPRanger(test.rejects, test.admits)

fakeClient := client.Fake{}
handler.SetOpenshiftClient(&fakeClient)
if test.admit {
// Ensure the requested address(s) is pre-allocated.
for _, ip := range test.externalIPs {
fakeClient.AddReactor("get", "externalips", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
return true, newExternalIP(ip, svc.ObjectMeta.Namespace, svc.ObjectMeta.Name), nil
})
}
}
err := handler.Admit(admission.NewAttributesRecord(svc, kapi.Kind("Service").WithVersion("version"), "namespace", svc.ObjectMeta.Name, kapi.Resource("services").WithVersion("version"), "", test.op, nil))
if test.admit && err != nil {
t.Errorf("%s: expected no error but got: %s", test.testName, err)
Expand All @@ -173,6 +200,22 @@ func TestAdmission(t *testing.T) {
}
}

func newExternalIP(ip, serviceNamespace, serviceName string) *externalipapi.ExternalIP {
return &externalipapi.ExternalIP{
TypeMeta: unversioned.TypeMeta{APIVersion: testapi.Default.GroupVersion().String()},
ObjectMeta: kapi.ObjectMeta{
UID: util.NewUUID(),
Name: ip,
ResourceVersion: "1",
},
ServiceRef: kapi.ObjectReference{
Namespace: serviceNamespace,
Name: serviceName,
Kind: "Service",
},
}
}

func TestHandles(t *testing.T) {
for op, shouldHandle := range map[admission.Operation]bool{
admission.Create: true,
Expand All @@ -186,3 +229,79 @@ func TestHandles(t *testing.T) {
}
}
}

func TestAllocateExternalIP(t *testing.T) {
ip := "172.16.0.1"
serviceNamespace := "default"
serviceName := "foo"

externalIP := newExternalIP(ip, serviceNamespace, serviceName)
othersExternalIP := newExternalIP(ip, serviceNamespace, "bar")

type funcReturns struct {
externalIP *externalipapi.ExternalIP
err error
}

tests := []struct {
testName string
getReturns *funcReturns
createReturns *funcReturns
allocated bool
}{
{
testName: "ip already allocated for target service",
getReturns: &funcReturns{
externalIP: externalIP,
},
allocated: true,
},
{
testName: "ip already allocated for another service",
getReturns: &funcReturns{
externalIP: othersExternalIP,
},
allocated: false,
},
{
testName: "ip allocated by another request for target service",
getReturns: &funcReturns{
err: kapierrors.NewNotFound(kapi.Resource("externalips"), ip),
},
createReturns: &funcReturns{
err: kapierrors.NewAlreadyExists(kapi.Resource("externalips"), ip),
},
allocated: false,
},
{
testName: "ip allocated",
getReturns: &funcReturns{
err: kapierrors.NewNotFound(kapi.Resource("externalips"), ip),
},
createReturns: &funcReturns{
err: nil,
},
allocated: true,
},
}
for _, test := range tests {
fakeClient := client.Fake{}
r := externalIPRanger{client: &fakeClient}
if test.getReturns != nil {
fakeClient.AddReactor("get", "externalips", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
return true, test.getReturns.externalIP, test.getReturns.err
})
}
if test.createReturns != nil {
fakeClient.AddReactor("create", "externalips", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
return true, test.createReturns.externalIP, test.createReturns.err
})
}
err := r.allocateExternalIP(externalIP.Name, externalIP.ServiceRef.Namespace, externalIP.ServiceRef.Name)
if test.allocated && err != nil {
t.Errorf("%s: expected no error but got: %s", test.testName, err)
} else if !test.allocated && err == nil {
t.Errorf("%s: expected an error", test.testName)
}
}
}

0 comments on commit 6e194c2

Please sign in to comment.