2014-06-06 23:40:48 +00:00
|
|
|
/*
|
|
|
|
Copyright 2014 Google 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.
|
|
|
|
*/
|
2014-06-18 21:18:53 +00:00
|
|
|
|
2014-08-11 07:34:59 +00:00
|
|
|
package service
|
2014-06-06 23:40:48 +00:00
|
|
|
|
|
|
|
import (
|
2014-06-17 17:50:42 +00:00
|
|
|
"fmt"
|
2014-08-25 21:36:15 +00:00
|
|
|
"math/rand"
|
2014-09-18 23:03:34 +00:00
|
|
|
"net"
|
2015-02-19 03:54:15 +00:00
|
|
|
"strconv"
|
2014-06-06 23:40:48 +00:00
|
|
|
|
2014-06-12 20:17:34 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
2014-09-03 21:16:00 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
|
2015-01-29 02:36:36 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
|
2014-09-01 05:10:49 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation"
|
2014-06-17 17:50:42 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider"
|
2014-06-17 02:10:43 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
2014-08-11 07:34:59 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/minion"
|
2014-09-06 02:22:03 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
2014-08-14 19:48:34 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
2014-09-18 23:03:34 +00:00
|
|
|
"github.com/golang/glog"
|
2014-06-06 23:40:48 +00:00
|
|
|
)
|
|
|
|
|
2014-09-08 21:40:56 +00:00
|
|
|
// REST adapts a service registry into apiserver's RESTStorage model.
|
|
|
|
type REST struct {
|
2014-09-18 23:03:34 +00:00
|
|
|
registry Registry
|
|
|
|
cloud cloudprovider.Interface
|
|
|
|
machines minion.Registry
|
|
|
|
portalMgr *ipAllocator
|
2014-06-06 23:40:48 +00:00
|
|
|
}
|
|
|
|
|
2014-09-08 21:40:56 +00:00
|
|
|
// NewREST returns a new REST.
|
2014-09-18 23:03:34 +00:00
|
|
|
func NewREST(registry Registry, cloud cloudprovider.Interface, machines minion.Registry, portalNet *net.IPNet) *REST {
|
|
|
|
// TODO: Before we can replicate masters, this has to be synced (e.g. lives in etcd)
|
|
|
|
ipa := newIPAllocator(portalNet)
|
2014-10-28 00:56:33 +00:00
|
|
|
if ipa == nil {
|
|
|
|
glog.Fatalf("Failed to create an IP allocator. Is subnet '%v' valid?", portalNet)
|
|
|
|
}
|
2014-09-18 23:03:34 +00:00
|
|
|
reloadIPsFromStorage(ipa, registry)
|
|
|
|
|
2014-09-08 21:40:56 +00:00
|
|
|
return &REST{
|
2014-09-18 23:03:34 +00:00
|
|
|
registry: registry,
|
|
|
|
cloud: cloud,
|
|
|
|
machines: machines,
|
|
|
|
portalMgr: ipa,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Helper: mark all previously allocated IPs in the allocator.
|
|
|
|
func reloadIPsFromStorage(ipa *ipAllocator, registry Registry) {
|
|
|
|
services, err := registry.ListServices(api.NewContext())
|
|
|
|
if err != nil {
|
|
|
|
// This is really bad.
|
2014-11-20 10:00:36 +00:00
|
|
|
glog.Errorf("can't list services to init service REST: %v", err)
|
2014-09-18 23:03:34 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
for i := range services.Items {
|
2014-10-22 17:02:02 +00:00
|
|
|
service := &services.Items[i]
|
2014-10-30 13:29:11 +00:00
|
|
|
if service.Spec.PortalIP == "" {
|
2014-10-22 17:02:02 +00:00
|
|
|
glog.Warningf("service %q has no PortalIP", service.Name)
|
2014-09-18 23:03:34 +00:00
|
|
|
continue
|
|
|
|
}
|
2014-10-30 13:29:11 +00:00
|
|
|
if err := ipa.Allocate(net.ParseIP(service.Spec.PortalIP)); err != nil {
|
2014-09-18 23:03:34 +00:00
|
|
|
// This is really bad.
|
2014-11-20 10:00:36 +00:00
|
|
|
glog.Errorf("service %q PortalIP %s could not be allocated: %v", service.Name, service.Spec.PortalIP, err)
|
2014-09-18 23:03:34 +00:00
|
|
|
}
|
2014-06-17 17:50:42 +00:00
|
|
|
}
|
2014-06-06 23:40:48 +00:00
|
|
|
}
|
|
|
|
|
2015-02-10 17:49:56 +00:00
|
|
|
func (rs *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) {
|
2014-10-22 17:02:02 +00:00
|
|
|
service := obj.(*api.Service)
|
2014-08-23 18:33:24 +00:00
|
|
|
|
2015-01-29 02:36:36 +00:00
|
|
|
if err := rest.BeforeCreate(rest.Services, ctx, obj); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2014-08-23 18:33:24 +00:00
|
|
|
|
2015-01-29 02:36:36 +00:00
|
|
|
if len(service.Spec.PortalIP) == 0 {
|
2014-10-24 17:28:39 +00:00
|
|
|
// Allocate next available.
|
2015-01-29 02:36:36 +00:00
|
|
|
ip, err := rs.portalMgr.AllocateNext()
|
|
|
|
if err != nil {
|
2014-10-24 17:28:39 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2015-01-29 02:36:36 +00:00
|
|
|
service.Spec.PortalIP = ip.String()
|
2014-09-18 23:03:34 +00:00
|
|
|
} else {
|
2014-10-24 17:28:39 +00:00
|
|
|
// Try to respect the requested IP.
|
2014-10-30 13:29:11 +00:00
|
|
|
if err := rs.portalMgr.Allocate(net.ParseIP(service.Spec.PortalIP)); err != nil {
|
2014-11-20 22:24:10 +00:00
|
|
|
el := errors.ValidationErrorList{errors.NewFieldInvalid("spec.portalIP", service.Spec.PortalIP, err.Error())}
|
2015-01-29 02:36:36 +00:00
|
|
|
return nil, errors.NewInvalid("Service", service.Name, el)
|
2014-10-24 17:28:39 +00:00
|
|
|
}
|
2014-09-18 23:03:34 +00:00
|
|
|
}
|
|
|
|
|
2015-02-10 17:49:56 +00:00
|
|
|
// TODO: Move this to post-creation rectification loop, so that we make/remove external load balancers
|
|
|
|
// correctly no matter what http operations happen.
|
|
|
|
if service.Spec.CreateExternalLoadBalancer {
|
2015-02-17 15:59:47 +00:00
|
|
|
err := rs.createExternalLoadBalancer(ctx, service)
|
2015-02-10 17:49:56 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
2015-01-29 02:36:36 +00:00
|
|
|
|
2015-02-10 17:49:56 +00:00
|
|
|
if err := rs.registry.CreateService(ctx, service); err != nil {
|
|
|
|
err = rest.CheckGeneratedNameError(rest.Services, err, service)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return rs.registry.GetService(ctx, service.Name)
|
2014-07-13 04:46:01 +00:00
|
|
|
}
|
|
|
|
|
2014-12-08 03:44:27 +00:00
|
|
|
func hostsFromMinionList(list *api.NodeList) []string {
|
2014-09-26 23:28:30 +00:00
|
|
|
result := make([]string, len(list.Items))
|
|
|
|
for ix := range list.Items {
|
2014-10-22 17:02:02 +00:00
|
|
|
result[ix] = list.Items[ix].Name
|
2014-09-26 23:28:30 +00:00
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2015-02-10 17:49:56 +00:00
|
|
|
func (rs *REST) Delete(ctx api.Context, id string) (runtime.Object, error) {
|
2014-09-26 19:18:42 +00:00
|
|
|
service, err := rs.registry.GetService(ctx, id)
|
2014-06-06 23:40:48 +00:00
|
|
|
if err != nil {
|
2014-08-11 07:34:59 +00:00
|
|
|
return nil, err
|
2014-06-06 23:40:48 +00:00
|
|
|
}
|
2014-10-30 13:29:11 +00:00
|
|
|
rs.portalMgr.Release(net.ParseIP(service.Spec.PortalIP))
|
2015-02-17 15:59:47 +00:00
|
|
|
if service.Spec.CreateExternalLoadBalancer {
|
|
|
|
rs.deleteExternalLoadBalancer(service)
|
|
|
|
}
|
2015-02-10 17:49:56 +00:00
|
|
|
return &api.Status{Status: api.StatusSuccess}, rs.registry.DeleteService(ctx, id)
|
2014-08-11 07:34:59 +00:00
|
|
|
}
|
|
|
|
|
2014-09-26 15:46:04 +00:00
|
|
|
func (rs *REST) Get(ctx api.Context, id string) (runtime.Object, error) {
|
2014-10-22 17:02:02 +00:00
|
|
|
service, err := rs.registry.GetService(ctx, id)
|
2014-08-11 07:34:59 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2014-06-06 23:40:48 +00:00
|
|
|
}
|
2014-10-22 17:02:02 +00:00
|
|
|
return service, err
|
2014-06-06 23:40:48 +00:00
|
|
|
}
|
|
|
|
|
2014-09-16 23:15:40 +00:00
|
|
|
// TODO: implement field selector?
|
2014-09-26 15:46:04 +00:00
|
|
|
func (rs *REST) List(ctx api.Context, label, field labels.Selector) (runtime.Object, error) {
|
2014-09-26 19:18:42 +00:00
|
|
|
list, err := rs.registry.ListServices(ctx)
|
2014-06-13 21:58:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2014-06-17 02:10:43 +00:00
|
|
|
var filtered []api.Service
|
|
|
|
for _, service := range list.Items {
|
2014-09-16 23:15:40 +00:00
|
|
|
if label.Matches(labels.Set(service.Labels)) {
|
2014-06-17 02:10:43 +00:00
|
|
|
filtered = append(filtered, service)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
list.Items = filtered
|
2014-06-08 04:07:20 +00:00
|
|
|
return list, err
|
2014-06-06 23:40:48 +00:00
|
|
|
}
|
|
|
|
|
2014-08-14 19:48:34 +00:00
|
|
|
// Watch returns Services events via a watch.Interface.
|
|
|
|
// It implements apiserver.ResourceWatcher.
|
2014-10-07 20:51:28 +00:00
|
|
|
func (rs *REST) Watch(ctx api.Context, label, field labels.Selector, resourceVersion string) (watch.Interface, error) {
|
2014-09-26 19:18:42 +00:00
|
|
|
return rs.registry.WatchServices(ctx, label, field, resourceVersion)
|
2014-08-14 19:48:34 +00:00
|
|
|
}
|
|
|
|
|
2014-09-08 21:40:56 +00:00
|
|
|
func (*REST) New() runtime.Object {
|
2014-08-11 07:34:59 +00:00
|
|
|
return &api.Service{}
|
|
|
|
}
|
|
|
|
|
2015-01-12 05:33:25 +00:00
|
|
|
func (*REST) NewList() runtime.Object {
|
2015-02-17 21:30:29 +00:00
|
|
|
return &api.ServiceList{}
|
2015-01-12 05:33:25 +00:00
|
|
|
}
|
|
|
|
|
2015-02-10 17:49:56 +00:00
|
|
|
func (rs *REST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) {
|
2014-10-22 17:02:02 +00:00
|
|
|
service := obj.(*api.Service)
|
2014-10-23 20:53:32 +00:00
|
|
|
if !api.ValidNamespace(ctx, &service.ObjectMeta) {
|
2015-02-10 17:49:56 +00:00
|
|
|
return nil, false, errors.NewConflict("service", service.Namespace, fmt.Errorf("Service.Namespace does not match the provided context"))
|
2014-09-29 21:17:42 +00:00
|
|
|
}
|
2015-01-27 23:55:54 +00:00
|
|
|
|
|
|
|
oldService, err := rs.registry.GetService(ctx, service.Name)
|
|
|
|
if err != nil {
|
2015-02-10 17:49:56 +00:00
|
|
|
return nil, false, err
|
2015-01-27 23:55:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Copy over non-user fields
|
|
|
|
// TODO: make this a merge function
|
|
|
|
if errs := validation.ValidateServiceUpdate(oldService, service); len(errs) > 0 {
|
2015-02-10 17:49:56 +00:00
|
|
|
return nil, false, errors.NewInvalid("service", service.Name, errs)
|
2014-08-11 07:34:59 +00:00
|
|
|
}
|
2015-02-17 15:59:47 +00:00
|
|
|
// Recreate external load balancer if changed.
|
|
|
|
if externalLoadBalancerNeedsUpdate(oldService, service) {
|
|
|
|
// TODO: support updating existing balancers
|
|
|
|
if oldService.Spec.CreateExternalLoadBalancer {
|
|
|
|
err = rs.deleteExternalLoadBalancer(oldService)
|
|
|
|
if err != nil {
|
|
|
|
return nil, false, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if service.Spec.CreateExternalLoadBalancer {
|
|
|
|
err = rs.createExternalLoadBalancer(ctx, service)
|
|
|
|
if err != nil {
|
|
|
|
return nil, false, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-02-10 17:49:56 +00:00
|
|
|
err = rs.registry.UpdateService(ctx, service)
|
|
|
|
if err != nil {
|
|
|
|
return nil, false, err
|
|
|
|
}
|
|
|
|
out, err := rs.registry.GetService(ctx, service.Name)
|
|
|
|
return out, false, err
|
2014-08-11 07:34:59 +00:00
|
|
|
}
|
2014-08-04 20:42:01 +00:00
|
|
|
|
2014-08-25 21:36:15 +00:00
|
|
|
// ResourceLocation returns a URL to which one can send traffic for the specified service.
|
2014-09-26 15:46:04 +00:00
|
|
|
func (rs *REST) ResourceLocation(ctx api.Context, id string) (string, error) {
|
2015-02-23 21:53:21 +00:00
|
|
|
eps, err := rs.registry.GetEndpoints(ctx, id)
|
2014-08-25 21:36:15 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2015-02-19 03:54:15 +00:00
|
|
|
if len(eps.Endpoints) == 0 {
|
2015-02-23 21:53:21 +00:00
|
|
|
return "", fmt.Errorf("no endpoints available for %v", id)
|
2014-08-25 21:36:15 +00:00
|
|
|
}
|
2014-10-17 21:21:53 +00:00
|
|
|
// We leave off the scheme ('http://') because we have no idea what sort of server
|
|
|
|
// is listening at this endpoint.
|
2015-02-23 21:53:21 +00:00
|
|
|
ep := &eps.Endpoints[rand.Intn(len(eps.Endpoints))]
|
|
|
|
return net.JoinHostPort(ep.IP, strconv.Itoa(ep.Port)), nil
|
2014-08-25 21:36:15 +00:00
|
|
|
}
|
|
|
|
|
2015-02-17 15:59:47 +00:00
|
|
|
func (rs *REST) createExternalLoadBalancer(ctx api.Context, service *api.Service) error {
|
|
|
|
if rs.cloud == nil {
|
|
|
|
return fmt.Errorf("requested an external service, but no cloud provider supplied.")
|
|
|
|
}
|
|
|
|
if service.Spec.Protocol != api.ProtocolTCP {
|
|
|
|
// TODO: Support UDP here too.
|
|
|
|
return fmt.Errorf("external load balancers for non TCP services are not currently supported.")
|
|
|
|
}
|
|
|
|
balancer, ok := rs.cloud.TCPLoadBalancer()
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("the cloud provider does not support external TCP load balancers.")
|
|
|
|
}
|
|
|
|
zones, ok := rs.cloud.Zones()
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("the cloud provider does not support zone enumeration.")
|
|
|
|
}
|
|
|
|
hosts, err := rs.machines.ListMinions(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
zone, err := zones.GetZone()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
// TODO: We should be able to rely on valid input, and not do defaulting here.
|
|
|
|
var affinityType api.AffinityType = service.Spec.SessionAffinity
|
|
|
|
if len(service.Spec.PublicIPs) > 0 {
|
|
|
|
for _, publicIP := range service.Spec.PublicIPs {
|
|
|
|
_, err = balancer.CreateTCPLoadBalancer(service.Name, zone.Region, net.ParseIP(publicIP), service.Spec.Port, hostsFromMinionList(hosts), affinityType)
|
|
|
|
if err != nil {
|
|
|
|
// TODO: have to roll-back any successful calls.
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ip, err := balancer.CreateTCPLoadBalancer(service.Name, zone.Region, nil, service.Spec.Port, hostsFromMinionList(hosts), affinityType)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
service.Spec.PublicIPs = []string{ip.String()}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2014-09-08 21:40:56 +00:00
|
|
|
func (rs *REST) deleteExternalLoadBalancer(service *api.Service) error {
|
2015-02-17 15:59:47 +00:00
|
|
|
if rs.cloud == nil {
|
|
|
|
return fmt.Errorf("requested an external service, but no cloud provider supplied.")
|
2014-08-11 07:34:59 +00:00
|
|
|
}
|
|
|
|
zones, ok := rs.cloud.Zones()
|
2014-08-04 20:42:01 +00:00
|
|
|
if !ok {
|
|
|
|
// We failed to get zone enumerator.
|
|
|
|
// As this should have failed when we tried in "create" too,
|
|
|
|
// assume external load balancer was never created.
|
|
|
|
return nil
|
|
|
|
}
|
2014-08-11 07:34:59 +00:00
|
|
|
balancer, ok := rs.cloud.TCPLoadBalancer()
|
2014-08-04 20:42:01 +00:00
|
|
|
if !ok {
|
|
|
|
// See comment above.
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
zone, err := zones.GetZone()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2014-10-22 17:02:02 +00:00
|
|
|
if err := balancer.DeleteTCPLoadBalancer(service.Name, zone.Region); err != nil {
|
2014-08-04 20:42:01 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2015-02-17 15:59:47 +00:00
|
|
|
|
|
|
|
func externalLoadBalancerNeedsUpdate(old, new *api.Service) bool {
|
|
|
|
if !old.Spec.CreateExternalLoadBalancer && !new.Spec.CreateExternalLoadBalancer {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if old.Spec.CreateExternalLoadBalancer != new.Spec.CreateExternalLoadBalancer ||
|
|
|
|
old.Spec.Port != new.Spec.Port ||
|
|
|
|
old.Spec.SessionAffinity != new.Spec.SessionAffinity ||
|
|
|
|
old.Spec.Protocol != new.Spec.Protocol {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if len(old.Spec.PublicIPs) != len(new.Spec.PublicIPs) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
for i := range old.Spec.PublicIPs {
|
|
|
|
if old.Spec.PublicIPs[i] != new.Spec.PublicIPs[i] {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|