Skip to content

Commit

Permalink
Headless Services: Adding option to specify None for PortalIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Abhishek Gupta committed Mar 20, 2015
1 parent 7f02e11 commit b0c23c1
Show file tree
Hide file tree
Showing 16 changed files with 237 additions and 16 deletions.
6 changes: 6 additions & 0 deletions cluster/addons/dns/kube2sky/kube2sky.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ func removeDNS(record string, 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{
Host: service.Spec.PortalIP,
Port: service.Spec.Port,
Expand Down
11 changes: 11 additions & 0 deletions pkg/api/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,14 @@ func IsStandardResourceName(str string) bool {
func NewDeleteOptions(grace int64) *DeleteOptions {
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 == ""
}
8 changes: 8 additions & 0 deletions pkg/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,12 @@ type ReplicationControllerList struct {
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.
type ServiceList struct {
TypeMeta `json:",inline"`
Expand Down Expand Up @@ -749,6 +755,8 @@ type ServiceSpec struct {
// 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
// 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"`

// CreateExternalLoadBalancer indicates whether a load balancer should be created for this service.
Expand Down
10 changes: 9 additions & 1 deletion pkg/api/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,12 @@ const (
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.
type ServiceList struct {
TypeMeta `json:",inline"`
Expand Down Expand Up @@ -615,7 +621,9 @@ type Service struct {
// 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
// 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.
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"`
Expand Down
10 changes: 9 additions & 1 deletion pkg/api/v1beta2/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,12 @@ const (
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.
type ServiceList struct {
TypeMeta `json:",inline"`
Expand Down Expand Up @@ -620,7 +626,9 @@ type Service struct {
// 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
// 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.
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"`
Expand Down
10 changes: 9 additions & 1 deletion pkg/api/v1beta3/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,9 @@ type ServiceSpec struct {
// 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
// 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 bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"`
Expand Down Expand Up @@ -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"`
}

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.
type ServiceList struct {
TypeMeta `json:",inline"`
Expand Down
11 changes: 9 additions & 2 deletions pkg/api/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package validation

import (
"fmt"
"net"
"path"
"strings"

Expand Down Expand Up @@ -751,6 +752,12 @@ func ValidateService(service *api.Service) errs.ValidationErrorList {
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
}

Expand All @@ -760,8 +767,8 @@ func ValidateServiceUpdate(oldService, service *api.Service) errs.ValidationErro
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
// PortalIP can only be set, not unset.
if oldService.Spec.PortalIP != "" && service.Spec.PortalIP != oldService.Spec.PortalIP {
// once PortalIP is set, it cannot be unset.
if api.IsServiceIPSet(oldService) && service.Spec.PortalIP != oldService.Spec.PortalIP {
allErrs = append(allErrs, errs.NewFieldInvalid("spec.portalIP", service.Spec.PortalIP, "field is immutable"))
}

Expand Down
21 changes: 21 additions & 0 deletions pkg/api/validation/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,13 @@ func TestValidateService(t *testing.T) {
},
numErrs: 1,
},
{
name: "invalid portal ip",
makeSvc: func(s *api.Service) {
s.Spec.PortalIP = "invalid"
},
numErrs: 1,
},
{
name: "missing port",
makeSvc: func(s *api.Service) {
Expand Down Expand Up @@ -1191,6 +1198,20 @@ func TestValidateService(t *testing.T) {
},
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 {
Expand Down
6 changes: 6 additions & 0 deletions pkg/kubelet/envvars/envvars.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ import (
func FromServices(services *api.ServiceList) []api.EnvVar {
var result []api.EnvVar
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
name := makeEnvVariableName(service.Name) + "_SERVICE_HOST"
result = append(result, api.EnvVar{Name: name, Value: service.Spec.PortalIP})
Expand Down
18 changes: 18 additions & 0 deletions pkg/kubelet/envvars/envvars_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,24 @@ func TestFromServices(t *testing.T) {
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)
Expand Down
4 changes: 4 additions & 0 deletions pkg/kubelet/kubelet.go
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,10 @@ func (kl *Kubelet) getServiceEnvVarMap(ns string) (map[string]string, error) {

// project the services in namespace ns onto the master services
for _, service := range services.Items {
// ignore services where PortalIP is "None" or empty
if !api.IsServiceIPSet(&service) {
continue
}
serviceName := service.Name

switch service.Namespace {
Expand Down
41 changes: 41 additions & 0 deletions pkg/kubelet/kubelet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1828,6 +1828,20 @@ func TestMakeEnvironmentVariables(t *testing.T) {
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"},
Spec: api.ServiceSpec{
Expand All @@ -1849,6 +1863,19 @@ func TestMakeEnvironmentVariables(t *testing.T) {
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"},
Spec: api.ServiceSpec{
Expand All @@ -1870,6 +1897,20 @@ func TestMakeEnvironmentVariables(t *testing.T) {
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 {
Expand Down
4 changes: 4 additions & 0 deletions pkg/proxy/proxier.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,10 @@ func (proxier *Proxier) OnUpdate(services []api.Service) {
glog.V(4).Infof("Received update notice: %+v", services)
activeServices := make(map[types.NamespacedName]bool) // use a map as a set
for _, service := range services {
// if PortalIP is "None" or empty, skip proxying
if !api.IsServiceIPSet(&service) {
continue
}
serviceName := types.NamespacedName{service.Namespace, service.Name}
activeServices[serviceName] = true
info, exists := proxier.getServiceInfo(serviceName)
Expand Down
Loading

0 comments on commit b0c23c1

Please sign in to comment.