Merge pull request #5556 from abhgupta/abhgupta-dev

Headless Services: Adding option to specify None for PortalIP
pull/6/head
Brian Grant 2015-03-20 14:06:55 -07:00
commit d6fe8924d9
16 changed files with 237 additions and 16 deletions

View File

@ -51,6 +51,12 @@ func removeDNS(record string, etcdClient *etcd.Client) error {
} }
func addDNS(record string, service *kapi.Service, etcdClient *etcd.Client) error { func addDNS(record string, service *kapi.Service, etcdClient *etcd.Client) error {
// if PortalIP is not set, a DNS entry should not be created
if !kapi.IsServiceIPSet(service) {
log.Printf("Skipping dns record for headless service: %s\n", service.Name)
return nil
}
svc := skymsg.Service{ svc := skymsg.Service{
Host: service.Spec.PortalIP, Host: service.Spec.PortalIP,
Port: service.Spec.Port, Port: service.Spec.Port,

View File

@ -84,3 +84,14 @@ func IsStandardResourceName(str string) bool {
func NewDeleteOptions(grace int64) *DeleteOptions { func NewDeleteOptions(grace int64) *DeleteOptions {
return &DeleteOptions{GracePeriodSeconds: &grace} return &DeleteOptions{GracePeriodSeconds: &grace}
} }
// this function aims to check if the service portal IP is set or not
// the objective is not to perform validation here
func IsServiceIPSet(service *Service) bool {
return service.Spec.PortalIP != PortalIPNone && service.Spec.PortalIP != ""
}
// this function aims to check if the service portal IP is requested or not
func IsServiceIPRequested(service *Service) bool {
return service.Spec.PortalIP == ""
}

View File

@ -710,6 +710,12 @@ type ReplicationControllerList struct {
Items []ReplicationController `json:"items"` Items []ReplicationController `json:"items"`
} }
const (
// PortalIPNone - do not assign a portal IP
// no proxying required and no environment variables should be created for pods
PortalIPNone = "None"
)
// ServiceList holds a list of services. // ServiceList holds a list of services.
type ServiceList struct { type ServiceList struct {
TypeMeta `json:",inline"` TypeMeta `json:",inline"`
@ -749,6 +755,8 @@ type ServiceSpec struct {
// PortalIP is usually assigned by the master. If specified by the user // PortalIP is usually assigned by the master. If specified by the user
// we will try to respect it or else fail the request. This field can // we will try to respect it or else fail the request. This field can
// not be changed by updates. // not be changed by updates.
// Valid values are None, empty string (""), or a valid IP address
// None can be specified for headless services when proxying is not required
PortalIP string `json:"portalIP,omitempty"` PortalIP string `json:"portalIP,omitempty"`
// CreateExternalLoadBalancer indicates whether a load balancer should be created for this service. // CreateExternalLoadBalancer indicates whether a load balancer should be created for this service.

View File

@ -578,6 +578,12 @@ const (
AffinityTypeNone AffinityType = "None" AffinityTypeNone AffinityType = "None"
) )
const (
// PortalIPNone - do not assign a portal IP
// no proxying required and no environment variables should be created for pods
PortalIPNone = "None"
)
// ServiceList holds a list of services. // ServiceList holds a list of services.
type ServiceList struct { type ServiceList struct {
TypeMeta `json:",inline"` TypeMeta `json:",inline"`
@ -615,7 +621,9 @@ type Service struct {
// PortalIP is usually assigned by the master. If specified by the user // PortalIP is usually assigned by the master. If specified by the user
// we will try to respect it or else fail the request. This field can // we will try to respect it or else fail the request. This field can
// not be changed by updates. // not be changed by updates.
PortalIP string `json:"portalIP,omitempty" description:"IP address of the service; usually assigned by the system; if specified, it will be allocated to the service if unused, and creation of the service will fail otherwise; cannot be updated"` // Valid values are None, empty string (""), or a valid IP address
// None can be specified for headless services when proxying is not required
PortalIP string `json:"portalIP,omitempty" description:"IP address of the service; usually assigned by the system; if specified, it will be allocated to the service if unused, and creation of the service will fail otherwise; cannot be updated; 'None' can be specified for a headless service when proxying is not required"`
// DEPRECATED: has no implementation. // DEPRECATED: has no implementation.
ProxyPort int `json:"proxyPort,omitempty" description:"if non-zero, a pre-allocated host port used for this service by the proxy on each node; assigned by the master and ignored on input"` ProxyPort int `json:"proxyPort,omitempty" description:"if non-zero, a pre-allocated host port used for this service by the proxy on each node; assigned by the master and ignored on input"`

View File

@ -581,6 +581,12 @@ const (
AffinityTypeNone AffinityType = "None" AffinityTypeNone AffinityType = "None"
) )
const (
// PortalIPNone - do not assign a portal IP
// no proxying required and no environment variables should be created for pods
PortalIPNone = "None"
)
// ServiceList holds a list of services. // ServiceList holds a list of services.
type ServiceList struct { type ServiceList struct {
TypeMeta `json:",inline"` TypeMeta `json:",inline"`
@ -620,7 +626,9 @@ type Service struct {
// PortalIP is usually assigned by the master. If specified by the user // PortalIP is usually assigned by the master. If specified by the user
// we will try to respect it or else fail the request. This field can // we will try to respect it or else fail the request. This field can
// not be changed by updates. // not be changed by updates.
PortalIP string `json:"portalIP,omitempty" description:"IP address of the service; usually assigned by the system; if specified, it will be allocated to the service if unused, and creation of the service will fail otherwise; cannot be updated"` // Valid values are None, empty string (""), or a valid IP address
// None can be specified for headless services when proxying is not required
PortalIP string `json:"portalIP,omitempty" description:"IP address of the service; usually assigned by the system; if specified, it will be allocated to the service if unused, and creation of the service will fail otherwise; cannot be updated; 'None' can be specified for a headless service when proxying is not required"`
// DEPRECATED: has no implementation. // DEPRECATED: has no implementation.
ProxyPort int `json:"proxyPort,omitempty" description:"if non-zero, a pre-allocated host port used for this service by the proxy on each node; assigned by the master and ignored on input"` ProxyPort int `json:"proxyPort,omitempty" description:"if non-zero, a pre-allocated host port used for this service by the proxy on each node; assigned by the master and ignored on input"`

View File

@ -743,7 +743,9 @@ type ServiceSpec struct {
// PortalIP is usually assigned by the master. If specified by the user // PortalIP is usually assigned by the master. If specified by the user
// we will try to respect it or else fail the request. This field can // we will try to respect it or else fail the request. This field can
// not be changed by updates. // not be changed by updates.
PortalIP string `json:"portalIP,omitempty description: IP address of the service; usually assigned by the system; if specified, it will be allocated to the service if unused, and creation of the service will fail otherwise; cannot be updated"` // Valid values are None, empty string (""), or a valid IP address
// None can be specified for headless services when proxying is not required
PortalIP string `json:"portalIP,omitempty description: IP address of the service; usually assigned by the system; if specified, it will be allocated to the service if unused, and creation of the service will fail otherwise; cannot be updated; 'None' can be specified for a headless service when proxying is not required"`
// CreateExternalLoadBalancer indicates whether a load balancer should be created for this service. // CreateExternalLoadBalancer indicates whether a load balancer should be created for this service.
CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"` CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"`
@ -775,6 +777,12 @@ type Service struct {
Status ServiceStatus `json:"status,omitempty" description:"most recently observed status of the service; populated by the system, read-only; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` Status ServiceStatus `json:"status,omitempty" description:"most recently observed status of the service; populated by the system, read-only; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"`
} }
const (
// PortalIPNone - do not assign a portal IP
// no proxying required and no environment variables should be created for pods
PortalIPNone = "None"
)
// ServiceList holds a list of services. // ServiceList holds a list of services.
type ServiceList struct { type ServiceList struct {
TypeMeta `json:",inline"` TypeMeta `json:",inline"`

View File

@ -18,6 +18,7 @@ package validation
import ( import (
"fmt" "fmt"
"net"
"path" "path"
"strings" "strings"
@ -751,6 +752,12 @@ func ValidateService(service *api.Service) errs.ValidationErrorList {
allErrs = append(allErrs, errs.NewFieldNotSupported("spec.sessionAffinity", service.Spec.SessionAffinity)) allErrs = append(allErrs, errs.NewFieldNotSupported("spec.sessionAffinity", service.Spec.SessionAffinity))
} }
if api.IsServiceIPSet(service) {
if ip := net.ParseIP(service.Spec.PortalIP); ip == nil {
allErrs = append(allErrs, errs.NewFieldInvalid("spec.portalIP", service.Spec.PortalIP, "portalIP should be empty, 'None', or a valid IP address"))
}
}
return allErrs return allErrs
} }
@ -760,8 +767,8 @@ func ValidateServiceUpdate(oldService, service *api.Service) errs.ValidationErro
allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldService.ObjectMeta, &service.ObjectMeta).Prefix("metadata")...) allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldService.ObjectMeta, &service.ObjectMeta).Prefix("metadata")...)
// TODO: PortalIP should be a Status field, since the system can set a value != to the user's value // TODO: PortalIP should be a Status field, since the system can set a value != to the user's value
// PortalIP can only be set, not unset. // once PortalIP is set, it cannot be unset.
if oldService.Spec.PortalIP != "" && service.Spec.PortalIP != oldService.Spec.PortalIP { if api.IsServiceIPSet(oldService) && service.Spec.PortalIP != oldService.Spec.PortalIP {
allErrs = append(allErrs, errs.NewFieldInvalid("spec.portalIP", service.Spec.PortalIP, "field is immutable")) allErrs = append(allErrs, errs.NewFieldInvalid("spec.portalIP", service.Spec.PortalIP, "field is immutable"))
} }

View File

@ -1134,6 +1134,13 @@ func TestValidateService(t *testing.T) {
}, },
numErrs: 1, numErrs: 1,
}, },
{
name: "invalid portal ip",
makeSvc: func(s *api.Service) {
s.Spec.PortalIP = "invalid"
},
numErrs: 1,
},
{ {
name: "missing port", name: "missing port",
makeSvc: func(s *api.Service) { makeSvc: func(s *api.Service) {
@ -1191,6 +1198,20 @@ func TestValidateService(t *testing.T) {
}, },
numErrs: 0, numErrs: 0,
}, },
{
name: "valid portal ip - none ",
makeSvc: func(s *api.Service) {
s.Spec.PortalIP = "None"
},
numErrs: 0,
},
{
name: "valid portal ip - empty",
makeSvc: func(s *api.Service) {
s.Spec.PortalIP = ""
},
numErrs: 0,
},
} }
for _, tc := range testCases { for _, tc := range testCases {

View File

@ -30,6 +30,12 @@ import (
func FromServices(services *api.ServiceList) []api.EnvVar { func FromServices(services *api.ServiceList) []api.EnvVar {
var result []api.EnvVar var result []api.EnvVar
for _, service := range services.Items { for _, service := range services.Items {
// ignore services where PortalIP is "None" or empty
// the services passed to this method should be pre-filtered
// only services that have the portal IP set should be included here
if !api.IsServiceIPSet(&service) {
continue
}
// Host // Host
name := makeEnvVariableName(service.Name) + "_SERVICE_HOST" name := makeEnvVariableName(service.Name) + "_SERVICE_HOST"
result = append(result, api.EnvVar{Name: name, Value: service.Spec.PortalIP}) result = append(result, api.EnvVar{Name: name, Value: service.Spec.PortalIP})

View File

@ -54,6 +54,24 @@ func TestFromServices(t *testing.T) {
PortalIP: "9.8.7.6", PortalIP: "9.8.7.6",
}, },
}, },
{
ObjectMeta: api.ObjectMeta{Name: "svrc-portalip-none"},
Spec: api.ServiceSpec{
Port: 8082,
Selector: map[string]string{"bar": "baz"},
Protocol: "TCP",
PortalIP: "None",
},
},
{
ObjectMeta: api.ObjectMeta{Name: "svrc-portalip-empty"},
Spec: api.ServiceSpec{
Port: 8082,
Selector: map[string]string{"bar": "baz"},
Protocol: "TCP",
PortalIP: "",
},
},
}, },
} }
vars := envvars.FromServices(&sl) vars := envvars.FromServices(&sl)

View File

@ -845,6 +845,10 @@ func (kl *Kubelet) getServiceEnvVarMap(ns string) (map[string]string, error) {
// project the services in namespace ns onto the master services // project the services in namespace ns onto the master services
for _, service := range services.Items { for _, service := range services.Items {
// ignore services where PortalIP is "None" or empty
if !api.IsServiceIPSet(&service) {
continue
}
serviceName := service.Name serviceName := service.Name
switch service.Namespace { switch service.Namespace {

View File

@ -1828,6 +1828,20 @@ func TestMakeEnvironmentVariables(t *testing.T) {
PortalIP: "1.2.3.2", PortalIP: "1.2.3.2",
}, },
}, },
{
ObjectMeta: api.ObjectMeta{Name: "kubernetes-ro", Namespace: api.NamespaceDefault},
Spec: api.ServiceSpec{
Port: 8082,
PortalIP: "None",
},
},
{
ObjectMeta: api.ObjectMeta{Name: "kubernetes-ro", Namespace: api.NamespaceDefault},
Spec: api.ServiceSpec{
Port: 8082,
PortalIP: "",
},
},
{ {
ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test1"}, ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test1"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
@ -1849,6 +1863,19 @@ func TestMakeEnvironmentVariables(t *testing.T) {
PortalIP: "1.2.3.5", PortalIP: "1.2.3.5",
}, },
}, },
{
ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test2"},
Spec: api.ServiceSpec{
Port: 8085,
PortalIP: "None",
},
},
{
ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test2"},
Spec: api.ServiceSpec{
Port: 8085,
},
},
{ {
ObjectMeta: api.ObjectMeta{Name: "kubernetes", Namespace: "kubernetes"}, ObjectMeta: api.ObjectMeta{Name: "kubernetes", Namespace: "kubernetes"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
@ -1870,6 +1897,20 @@ func TestMakeEnvironmentVariables(t *testing.T) {
PortalIP: "1.2.3.8", PortalIP: "1.2.3.8",
}, },
}, },
{
ObjectMeta: api.ObjectMeta{Name: "not-special", Namespace: "kubernetes"},
Spec: api.ServiceSpec{
Port: 8088,
PortalIP: "None",
},
},
{
ObjectMeta: api.ObjectMeta{Name: "not-special", Namespace: "kubernetes"},
Spec: api.ServiceSpec{
Port: 8088,
PortalIP: "",
},
},
} }
testCases := []struct { testCases := []struct {

View File

@ -468,6 +468,10 @@ func (proxier *Proxier) OnUpdate(services []api.Service) {
glog.V(4).Infof("Received update notice: %+v", services) glog.V(4).Infof("Received update notice: %+v", services)
activeServices := make(map[types.NamespacedName]bool) // use a map as a set activeServices := make(map[types.NamespacedName]bool) // use a map as a set
for _, service := range services { for _, service := range services {
// if PortalIP is "None" or empty, skip proxying
if !api.IsServiceIPSet(&service) {
continue
}
serviceName := types.NamespacedName{service.Namespace, service.Name} serviceName := types.NamespacedName{service.Namespace, service.Name}
activeServices[serviceName] = true activeServices[serviceName] = true
info, exists := proxier.getServiceInfo(serviceName) info, exists := proxier.getServiceInfo(serviceName)

View File

@ -400,7 +400,7 @@ func TestTCPProxyUpdateDeleteUpdate(t *testing.T) {
} }
waitForNumProxyLoops(t, p, 0) waitForNumProxyLoops(t, p, 0)
p.OnUpdate([]api.Service{ p.OnUpdate([]api.Service{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP"}, Status: api.ServiceStatus{}}, {ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP", PortalIP: "1.2.3.4"}, Status: api.ServiceStatus{}},
}) })
svcInfo, exists := p.getServiceInfo(service) svcInfo, exists := p.getServiceInfo(service)
if !exists { if !exists {
@ -440,7 +440,7 @@ func TestUDPProxyUpdateDeleteUpdate(t *testing.T) {
} }
waitForNumProxyLoops(t, p, 0) waitForNumProxyLoops(t, p, 0)
p.OnUpdate([]api.Service{ p.OnUpdate([]api.Service{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "UDP"}, Status: api.ServiceStatus{}}, {ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "UDP", PortalIP: "1.2.3.4"}, Status: api.ServiceStatus{}},
}) })
svcInfo, exists := p.getServiceInfo(service) svcInfo, exists := p.getServiceInfo(service)
if !exists { if !exists {
@ -471,7 +471,7 @@ func TestTCPProxyUpdatePort(t *testing.T) {
waitForNumProxyLoops(t, p, 1) waitForNumProxyLoops(t, p, 1)
p.OnUpdate([]api.Service{ p.OnUpdate([]api.Service{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: 99, Protocol: "TCP"}, Status: api.ServiceStatus{}}, {ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: 99, Protocol: "TCP", PortalIP: "1.2.3.4"}, Status: api.ServiceStatus{}},
}) })
// Wait for the socket to actually get free. // Wait for the socket to actually get free.
if err := waitForClosedPortTCP(p, svcInfo.proxyPort); err != nil { if err := waitForClosedPortTCP(p, svcInfo.proxyPort); err != nil {
@ -507,7 +507,7 @@ func TestUDPProxyUpdatePort(t *testing.T) {
waitForNumProxyLoops(t, p, 1) waitForNumProxyLoops(t, p, 1)
p.OnUpdate([]api.Service{ p.OnUpdate([]api.Service{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: 99, Protocol: "UDP"}, Status: api.ServiceStatus{}}, {ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: 99, Protocol: "UDP", PortalIP: "1.2.3.4"}, Status: api.ServiceStatus{}},
}) })
// Wait for the socket to actually get free. // Wait for the socket to actually get free.
if err := waitForClosedPortUDP(p, svcInfo.proxyPort); err != nil { if err := waitForClosedPortUDP(p, svcInfo.proxyPort); err != nil {
@ -521,4 +521,59 @@ func TestUDPProxyUpdatePort(t *testing.T) {
waitForNumProxyLoops(t, p, 1) waitForNumProxyLoops(t, p, 1)
} }
func TestProxyUpdatePortal(t *testing.T) {
lb := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "echo")
lb.OnUpdate([]api.Endpoints{
{
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
Endpoints: []api.Endpoint{{IP: "127.0.0.1", Port: tcpServerPort}},
},
})
p := CreateProxier(lb, net.ParseIP("0.0.0.0"), &fakeIptables{}, net.ParseIP("127.0.0.1"))
waitForNumProxyLoops(t, p, 0)
svcInfo, err := p.addServiceOnPort(service, "TCP", 0, time.Second)
if err != nil {
t.Fatalf("error adding new service: %#v", err)
}
testEchoTCP(t, "127.0.0.1", svcInfo.proxyPort)
waitForNumProxyLoops(t, p, 1)
p.OnUpdate([]api.Service{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP"}, Status: api.ServiceStatus{}},
})
_, exists := p.getServiceInfo(service)
if exists {
t.Fatalf("service without portalIP should not be included in the proxy")
}
p.OnUpdate([]api.Service{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP", PortalIP: ""}, Status: api.ServiceStatus{}},
})
_, exists = p.getServiceInfo(service)
if exists {
t.Fatalf("service with empty portalIP should not be included in the proxy")
}
p.OnUpdate([]api.Service{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP", PortalIP: "None"}, Status: api.ServiceStatus{}},
})
_, exists = p.getServiceInfo(service)
if exists {
t.Fatalf("service with 'None' as portalIP should not be included in the proxy")
}
p.OnUpdate([]api.Service{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP", PortalIP: "1.2.3.4"}, Status: api.ServiceStatus{}},
})
svcInfo, exists = p.getServiceInfo(service)
if !exists {
t.Fatalf("service with portalIP set not found in the proxy")
}
testEchoTCP(t, "127.0.0.1", svcInfo.proxyPort)
waitForNumProxyLoops(t, p, 1)
}
// TODO: Test UDP timeouts. // TODO: Test UDP timeouts.

View File

@ -73,8 +73,7 @@ func reloadIPsFromStorage(ipa *ipAllocator, registry Registry) {
} }
for i := range services.Items { for i := range services.Items {
service := &services.Items[i] service := &services.Items[i]
if service.Spec.PortalIP == "" { if !api.IsServiceIPSet(service) {
glog.Warningf("service %q has no PortalIP", service.Name)
continue continue
} }
if err := ipa.Allocate(net.ParseIP(service.Spec.PortalIP)); err != nil { if err := ipa.Allocate(net.ParseIP(service.Spec.PortalIP)); err != nil {
@ -91,14 +90,14 @@ func (rs *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, err
return nil, err return nil, err
} }
if len(service.Spec.PortalIP) == 0 { if api.IsServiceIPRequested(service) {
// Allocate next available. // Allocate next available.
ip, err := rs.portalMgr.AllocateNext() ip, err := rs.portalMgr.AllocateNext()
if err != nil { if err != nil {
return nil, err return nil, err
} }
service.Spec.PortalIP = ip.String() service.Spec.PortalIP = ip.String()
} else { } else if api.IsServiceIPSet(service) {
// Try to respect the requested IP. // Try to respect the requested IP.
if err := rs.portalMgr.Allocate(net.ParseIP(service.Spec.PortalIP)); err != nil { if err := rs.portalMgr.Allocate(net.ParseIP(service.Spec.PortalIP)); err != nil {
el := errors.ValidationErrorList{errors.NewFieldInvalid("spec.portalIP", service.Spec.PortalIP, err.Error())} el := errors.ValidationErrorList{errors.NewFieldInvalid("spec.portalIP", service.Spec.PortalIP, err.Error())}
@ -111,14 +110,18 @@ func (rs *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, err
if service.Spec.CreateExternalLoadBalancer { if service.Spec.CreateExternalLoadBalancer {
err := rs.createExternalLoadBalancer(ctx, service) err := rs.createExternalLoadBalancer(ctx, service)
if err != nil { if err != nil {
if api.IsServiceIPSet(service) {
rs.portalMgr.Release(net.ParseIP(service.Spec.PortalIP)) rs.portalMgr.Release(net.ParseIP(service.Spec.PortalIP))
}
return nil, err return nil, err
} }
} }
out, err := rs.registry.CreateService(ctx, service) out, err := rs.registry.CreateService(ctx, service)
if err != nil { if err != nil {
if api.IsServiceIPSet(service) {
rs.portalMgr.Release(net.ParseIP(service.Spec.PortalIP)) rs.portalMgr.Release(net.ParseIP(service.Spec.PortalIP))
}
err = rest.CheckGeneratedNameError(rest.Services, err, service) err = rest.CheckGeneratedNameError(rest.Services, err, service)
} }
return out, err return out, err
@ -137,7 +140,9 @@ func (rs *REST) Delete(ctx api.Context, id string) (runtime.Object, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if api.IsServiceIPSet(service) {
rs.portalMgr.Release(net.ParseIP(service.Spec.PortalIP)) rs.portalMgr.Release(net.ParseIP(service.Spec.PortalIP))
}
if service.Spec.CreateExternalLoadBalancer { if service.Spec.CreateExternalLoadBalancer {
rs.deleteExternalLoadBalancer(ctx, service) rs.deleteExternalLoadBalancer(ctx, service)
} }

View File

@ -735,6 +735,7 @@ func TestCreate(t *testing.T) {
&api.Service{ &api.Service{
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
PortalIP: "None",
Port: 6502, Port: 6502,
Protocol: "TCP", Protocol: "TCP",
SessionAffinity: "None", SessionAffinity: "None",
@ -744,5 +745,15 @@ func TestCreate(t *testing.T) {
&api.Service{ &api.Service{
Spec: api.ServiceSpec{}, Spec: api.ServiceSpec{},
}, },
// invalid
&api.Service{
Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"},
Port: 6502,
Protocol: "TCP",
PortalIP: "invalid",
SessionAffinity: "None",
},
},
) )
} }