From c847f5e5a504820718c5d6d3934274f91e0657e0 Mon Sep 17 00:00:00 2001 From: Pengfei Ni Date: Wed, 26 Jul 2017 10:36:45 +0800 Subject: [PATCH] Add service controller - Add a service controller in stackube and create lbaas v2 pools for new services, also add members for endpoints. - Fix getting network for system namespaces. Change-Id: I7942a2d26dd33b4ceb75ec51c03933205a60aea7 Implements: blueprint service-loadbalancer Signed-off-by: Pengfei Ni --- .../stackube-controller.go | 63 +- .../rbacmanager/rbac_controller.go | 18 +- .../tenant/tenant_controller.go | 34 +- pkg/network-controller/network_controller.go | 72 +- pkg/openstack/loadbalancer.go | 766 ++++++++++++++++++ pkg/proxy/helpers.go | 9 - pkg/proxy/proxier.go | 16 +- pkg/proxy/types.go | 3 +- pkg/service-controller/helpers.go | 19 + pkg/service-controller/service_controller.go | 671 +++++++++++++++ pkg/util/k8s_util.go | 36 + pkg/util/util.go | 4 +- .../extensions/layer3/floatingips/requests.go | 146 ++++ .../extensions/layer3/floatingips/results.go | 110 +++ .../v2/extensions/layer3/floatingips/urls.go | 13 + .../extensions/lbaas_v2/listeners/requests.go | 182 +++++ .../extensions/lbaas_v2/listeners/results.go | 114 +++ .../v2/extensions/lbaas_v2/listeners/urls.go | 16 + .../lbaas_v2/loadbalancers/requests.go | 172 ++++ .../lbaas_v2/loadbalancers/results.go | 125 +++ .../extensions/lbaas_v2/loadbalancers/urls.go | 21 + .../extensions/lbaas_v2/monitors/requests.go | 233 ++++++ .../extensions/lbaas_v2/monitors/results.go | 144 ++++ .../v2/extensions/lbaas_v2/monitors/urls.go | 16 + .../v2/extensions/lbaas_v2/pools/requests.go | 334 ++++++++ .../v2/extensions/lbaas_v2/pools/results.go | 242 ++++++ .../v2/extensions/lbaas_v2/pools/urls.go | 25 + vendor/vendor.json | 30 + 28 files changed, 3504 insertions(+), 130 deletions(-) create mode 100644 pkg/openstack/loadbalancer.go create mode 100644 pkg/service-controller/helpers.go create mode 100644 pkg/service-controller/service_controller.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/urls.go diff --git a/cmd/stackube-controller/stackube-controller.go b/cmd/stackube-controller/stackube-controller.go index 26a64fa..c820b72 100644 --- a/cmd/stackube-controller/stackube-controller.go +++ b/cmd/stackube-controller/stackube-controller.go @@ -11,12 +11,15 @@ import ( "git.openstack.org/openstack/stackube/pkg/auth-controller/tenant" "git.openstack.org/openstack/stackube/pkg/network-controller" "git.openstack.org/openstack/stackube/pkg/openstack" + "git.openstack.org/openstack/stackube/pkg/service-controller" "git.openstack.org/openstack/stackube/pkg/util" + extclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/client-go/kubernetes" + "github.com/golang/glog" "github.com/spf13/pflag" "golang.org/x/sync/errgroup" - "k8s.io/client-go/kubernetes" ) var ( @@ -28,26 +31,28 @@ var ( userGateway = pflag.String("user-gateway", "10.244.0.1", "user Pod network gateway") ) -func startControllers(kubeconfig, cloudconfig string) error { +func startControllers(kubeClient *kubernetes.Clientset, + osClient *openstack.Client, kubeExtClient *extclientset.Clientset) error { // Creates a new Tenant controller - tc, err := tenant.NewTenantController(kubeconfig, cloudconfig) + tenantController, err := tenant.NewTenantController(kubeClient, osClient, kubeExtClient) if err != nil { return err } // Creates a new Network controller - nc, err := network.NewNetworkController( - kubeconfig, cloudconfig) + networkController, err := network.NewNetworkController(osClient, kubeExtClient) if err != nil { return err } // Creates a new RBAC controller - rm, err := rbacmanager.NewRBACController(kubeconfig, - tc.GetKubeCRDClient(), - *userCIDR, - *userGateway, - ) + rbacController, err := rbacmanager.NewRBACController(kubeClient, osClient.CRDClient, *userCIDR, *userGateway) + if err != nil { + return err + } + + // Creates a new service controller + serviceController, err := service.NewServiceController(kubeClient, osClient) if err != nil { return err } @@ -56,11 +61,14 @@ func startControllers(kubeconfig, cloudconfig string) error { wg, ctx := errgroup.WithContext(ctx) // start auth controllers in stackube - wg.Go(func() error { return tc.Run(ctx.Done()) }) - wg.Go(func() error { return rm.Run(ctx.Done()) }) + wg.Go(func() error { return tenantController.Run(ctx.Done()) }) + wg.Go(func() error { return rbacController.Run(ctx.Done()) }) // start network controller - wg.Go(func() error { return nc.Run(ctx.Done()) }) + wg.Go(func() error { return networkController.Run(ctx.Done()) }) + + // start service controller + wg.Go(func() error { return serviceController.Run(ctx.Done()) }) term := make(chan os.Signal) signal.Notify(term, os.Interrupt, syscall.SIGTERM) @@ -80,23 +88,28 @@ func startControllers(kubeconfig, cloudconfig string) error { return nil } -func verifyClientSetting() error { +func initClients() (*kubernetes.Clientset, *openstack.Client, *extclientset.Clientset, error) { + // Create kubernetes client config. Use kubeconfig if given, otherwise assume in-cluster. config, err := util.NewClusterConfig(*kubeconfig) if err != nil { - return fmt.Errorf("Init kubernetes cluster failed: %v", err) + return nil, nil, nil, fmt.Errorf("failed to build kubeconfig: %v", err) } - - _, err = kubernetes.NewForConfig(config) + kubeClient, err := kubernetes.NewForConfig(config) if err != nil { - return fmt.Errorf("Init kubernetes clientset failed: %v", err) + return nil, nil, nil, fmt.Errorf("failed to create kubernetes clientset: %v", err) } - - _, err = openstack.NewClient(*cloudconfig, *kubeconfig) + kubeExtClient, err := extclientset.NewForConfig(config) if err != nil { - return fmt.Errorf("Init openstack client failed: %v", err) + return nil, nil, nil, fmt.Errorf("failed to create kubernetes apiextensions clientset: %v", err) } - return nil + // Create OpenStack client from config file. + osClient, err := openstack.NewClient(*cloudconfig, *kubeconfig) + if err != nil { + return nil, nil, nil, fmt.Errorf("could't initialize openstack client: %v", err) + } + + return kubeClient, osClient, kubeExtClient, nil } func main() { @@ -104,14 +117,14 @@ func main() { util.InitLogs() defer util.FlushLogs() - // Verify client setting at the beginning and fail early if there are errors. - err := verifyClientSetting() + // Initilize kubernetes and openstack clients. + kubeClient, osClient, kubeExtClient, err := initClients() if err != nil { glog.Fatal(err) } // Start stackube controllers. - if err := startControllers(*kubeconfig, *cloudconfig); err != nil { + if err := startControllers(kubeClient, osClient, kubeExtClient); err != nil { glog.Fatal(err) } } diff --git a/pkg/auth-controller/rbacmanager/rbac_controller.go b/pkg/auth-controller/rbacmanager/rbac_controller.go index a348fe8..1ce8d09 100644 --- a/pkg/auth-controller/rbacmanager/rbac_controller.go +++ b/pkg/auth-controller/rbacmanager/rbac_controller.go @@ -34,22 +34,10 @@ type Controller struct { } // New creates a new RBAC controller. -func NewRBACController(kubeconfig string, - kubeCRDClient *crdClient.CRDClient, - userCIDR string, - userGateway string, -) (*Controller, error) { - cfg, err := util.NewClusterConfig(kubeconfig) - if err != nil { - return nil, fmt.Errorf("init cluster config failed: %v", err) - } - client, err := kubernetes.NewForConfig(cfg) - if err != nil { - return nil, fmt.Errorf("init kubernetes client failed: %v", err) - } - +func NewRBACController(kubeClient *kubernetes.Clientset, kubeCRDClient *crdClient.CRDClient, userCIDR string, + userGateway string) (*Controller, error) { o := &Controller{ - kclient: client, + kclient: kubeClient, queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "rbacmanager"), kubeCRDClient: kubeCRDClient, userCIDR: userCIDR, diff --git a/pkg/auth-controller/tenant/tenant_controller.go b/pkg/auth-controller/tenant/tenant_controller.go index 3b750fe..103cb47 100644 --- a/pkg/auth-controller/tenant/tenant_controller.go +++ b/pkg/auth-controller/tenant/tenant_controller.go @@ -7,7 +7,6 @@ import ( crdClient "git.openstack.org/openstack/stackube/pkg/kubecrd" "git.openstack.org/openstack/stackube/pkg/openstack" - "git.openstack.org/openstack/stackube/pkg/util" "github.com/golang/glog" apiv1 "k8s.io/api/core/v1" apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" @@ -27,38 +26,19 @@ type TenantController struct { } // NewTenantController creates a new tenant controller. -func NewTenantController(kubeconfig, cloudconfig string) (*TenantController, error) { - // Create the client config. Use kubeconfig if given, otherwise assume in-cluster. - config, err := util.NewClusterConfig(kubeconfig) - if err != nil { - return nil, fmt.Errorf("failed to build kubeconfig: %v", err) - } - clientset, err := apiextensionsclient.NewForConfig(config) - if err != nil { - return nil, fmt.Errorf("failed to create kubeclient from config: %v", err) - } - +func NewTenantController(kubeClient *kubernetes.Clientset, + osClient *openstack.Client, + kubeExtClient *apiextensionsclient.Clientset) (*TenantController, error) { // initialize CRD if it does not exist - _, err = crdClient.CreateTenantCRD(clientset) + _, err := crdClient.CreateTenantCRD(kubeExtClient) if err != nil && !apierrors.IsAlreadyExists(err) { return nil, fmt.Errorf("failed to create CRD to kube-apiserver: %v", err) } - k8sClient, err := kubernetes.NewForConfig(config) - if err != nil { - return nil, fmt.Errorf("failed to create kubernetes client: %v", err) - } - - // Create OpenStack client from config - openStackClient, err := openstack.NewClient(cloudconfig, kubeconfig) - if err != nil { - return nil, fmt.Errorf("init openstack client failed: %v", err) - } - c := &TenantController{ - kubeCRDClient: openStackClient.CRDClient, - k8sClient: k8sClient, - openstackClient: openStackClient, + kubeCRDClient: osClient.CRDClient, + k8sClient: kubeClient, + openstackClient: osClient, } if err = c.createClusterRoles(); err != nil { diff --git a/pkg/network-controller/network_controller.go b/pkg/network-controller/network_controller.go index f70c56a..269e045 100644 --- a/pkg/network-controller/network_controller.go +++ b/pkg/network-controller/network_controller.go @@ -3,6 +3,7 @@ package network import ( "fmt" + "github.com/golang/glog" apiv1 "k8s.io/api/core/v1" apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -11,75 +12,58 @@ import ( "k8s.io/client-go/tools/cache" crv1 "git.openstack.org/openstack/stackube/pkg/apis/v1" - crdClient "git.openstack.org/openstack/stackube/pkg/kubecrd" - osDriver "git.openstack.org/openstack/stackube/pkg/openstack" + "git.openstack.org/openstack/stackube/pkg/kubecrd" + "git.openstack.org/openstack/stackube/pkg/openstack" "git.openstack.org/openstack/stackube/pkg/util" - - "github.com/golang/glog" ) // Watcher is an network of watching on resource create/update/delete events type NetworkController struct { - kubeCRDClient *crdClient.CRDClient - driver *osDriver.Client + kubeCRDClient *kubecrd.CRDClient + driver *openstack.Client + networkInformer cache.Controller } -func (c *NetworkController) GetKubeCRDClient() *crdClient.CRDClient { +func (c *NetworkController) GetKubeCRDClient() *kubecrd.CRDClient { return c.kubeCRDClient } func (c *NetworkController) Run(stopCh <-chan struct{}) error { defer utilruntime.HandleCrash() - source := cache.NewListWatchFromClient( - c.kubeCRDClient.Client, - crv1.NetworkResourcePlural, - apiv1.NamespaceAll, - fields.Everything()) - - _, networkInformer := cache.NewInformer( - source, - &crv1.Network{}, - 0, - cache.ResourceEventHandlerFuncs{ - AddFunc: c.onAdd, - UpdateFunc: c.onUpdate, - DeleteFunc: c.onDelete, - }) - - go networkInformer.Run(stopCh) + go c.networkInformer.Run(stopCh) <-stopCh return nil } -func NewNetworkController(kubeconfig, openstackConfigFile string) (*NetworkController, error) { - // Create the client config. Use kubeconfig if given, otherwise assume in-cluster. - config, err := util.NewClusterConfig(kubeconfig) - if err != nil { - return nil, fmt.Errorf("failed to build kubeconfig: %v", err) - } - clientset, err := apiextensionsclient.NewForConfig(config) - if err != nil { - return nil, fmt.Errorf("failed to create kubeclient from config: %v", err) - } - +func NewNetworkController(osClient *openstack.Client, kubeExtClient *apiextensionsclient.Clientset) (*NetworkController, error) { // initialize CRD if it does not exist - _, err = crdClient.CreateNetworkCRD(clientset) + _, err := kubecrd.CreateNetworkCRD(kubeExtClient) if err != nil && !apierrors.IsAlreadyExists(err) { return nil, fmt.Errorf("failed to create CRD to kube-apiserver: %v", err) } - // Create OpenStack client from config - openstack, err := osDriver.NewClient(openstackConfigFile, kubeconfig) - if err != nil { - return nil, fmt.Errorf("Couldn't initialize openstack: %#v", err) - } - + source := cache.NewListWatchFromClient( + osClient.CRDClient.Client, + crv1.NetworkResourcePlural, + apiv1.NamespaceAll, + fields.Everything()) networkController := &NetworkController{ - kubeCRDClient: openstack.CRDClient, - driver: openstack, + kubeCRDClient: osClient.CRDClient, + driver: osClient, } + _, networkInformer := cache.NewInformer( + source, + &crv1.Network{}, + 0, + cache.ResourceEventHandlerFuncs{ + AddFunc: networkController.onAdd, + UpdateFunc: networkController.onUpdate, + DeleteFunc: networkController.onDelete, + }) + networkController.networkInformer = networkInformer + return networkController, nil } diff --git a/pkg/openstack/loadbalancer.go b/pkg/openstack/loadbalancer.go new file mode 100644 index 0000000..3533a8c --- /dev/null +++ b/pkg/openstack/loadbalancer.go @@ -0,0 +1,766 @@ +package openstack + +import ( + "fmt" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + + "github.com/golang/glog" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools" + "github.com/gophercloud/gophercloud/pagination" +) + +const ( + defaultMonitorDelay = 10 + defaultMonitorRetry = 3 + defaultMonotorTimeout = 3 + + // loadbalancerActive* is configuration of exponential backoff for + // going into ACTIVE loadbalancer provisioning status. Starting with 1 + // seconds, multiplying by 1.2 with each step and taking 19 steps at maximum + // it will time out after 128s, which roughly corresponds to 120s + loadbalancerActiveInitDealy = 1 * time.Second + loadbalancerActiveFactor = 1.2 + loadbalancerActiveSteps = 19 + + // loadbalancerDelete* is configuration of exponential backoff for + // waiting for delete operation to complete. Starting with 1 + // seconds, multiplying by 1.2 with each step and taking 13 steps at maximum + // it will time out after 32s, which roughly corresponds to 30s + loadbalancerDeleteInitDealy = 1 * time.Second + loadbalancerDeleteFactor = 1.2 + loadbalancerDeleteSteps = 13 + + activeStatus = "ACTIVE" + errorStatus = "ERROR" +) + +// LoadBalancer contains all essential information of kubernetes service. +type LoadBalancer struct { + Name string + ServicePort int + TenantID string + SubnetID string + Protocol string + InternalIP string + ExternalIP string + SessionAffinity bool + Endpoints []Endpoint +} + +// Endpoint represents a container endpoint. +type Endpoint struct { + Address string + Port int +} + +// LoadBalancerStatus contains the status of a load balancer. +type LoadBalancerStatus struct { + InternalIP string + ExternalIP string +} + +// EnsureLoadBalancer ensures a load balancer is created. +func (os *Client) EnsureLoadBalancer(lb *LoadBalancer) (*LoadBalancerStatus, error) { + // removes old one if already exists. + loadbalancer, err := os.getLoadBalanceByName(lb.Name) + if err != nil { + if isNotFound(err) { + // create a new one. + lbOpts := loadbalancers.CreateOpts{ + Name: lb.Name, + Description: "Stackube service", + VipSubnetID: lb.SubnetID, + TenantID: lb.TenantID, + } + loadbalancer, err = loadbalancers.Create(os.Network, lbOpts).Extract() + if err != nil { + glog.Errorf("Create load balancer %q failed: %v", lb.Name, err) + return nil, err + } + } + } else { + glog.V(3).Infof("LoadBalancer %s already exists", lb.Name) + } + + status, err := os.waitLoadBalancerStatus(loadbalancer.ID) + if err != nil { + glog.Errorf("Waiting for load balancer provision failed: %v", err) + return nil, err + } + + glog.V(3).Infof("Load balancer %q becomes %q", lb.Name, status) + + // get old listeners + var listener *listeners.Listener + oldListeners, err := os.getListenersByLoadBalancerID(loadbalancer.ID) + if err != nil { + return nil, fmt.Errorf("error getting LB %s listeners: %v", loadbalancer.Name, err) + } + for i := range oldListeners { + l := oldListeners[i] + if l.ProtocolPort == lb.ServicePort { + listener = &l + } else { + // delete the obsolete listener + if err := os.ensureListenerDeleted(loadbalancer.ID, l); err != nil { + return nil, fmt.Errorf("error deleting listener %q: %v", l.Name, err) + } + os.waitLoadBalancerStatus(loadbalancer.ID) + } + } + + // create the listener. + if listener == nil { + lisOpts := listeners.CreateOpts{ + LoadbalancerID: loadbalancer.ID, + // Only tcp is supported now. + Protocol: listeners.ProtocolTCP, + ProtocolPort: lb.ServicePort, + TenantID: lb.TenantID, + Name: lb.Name, + } + listener, err = listeners.Create(os.Network, lisOpts).Extract() + if err != nil { + glog.Errorf("Create listener %q failed: %v", lb.Name, err) + return nil, err + } + os.waitLoadBalancerStatus(loadbalancer.ID) + } + + // create the load balancer pool. + pool, err := os.getPoolByListenerID(loadbalancer.ID, listener.ID) + if err != nil && !isNotFound(err) { + return nil, fmt.Errorf("error getting pool for listener %q: %v", listener.ID, err) + } + if pool == nil { + poolOpts := pools.CreateOpts{ + Name: lb.Name, + ListenerID: listener.ID, + Protocol: pools.ProtocolTCP, + LBMethod: pools.LBMethodRoundRobin, + TenantID: lb.TenantID, + } + if lb.SessionAffinity { + poolOpts.Persistence = &pools.SessionPersistence{Type: "SOURCE_IP"} + } + pool, err = pools.Create(os.Network, poolOpts).Extract() + if err != nil { + glog.Errorf("Create pool %q failed: %v", lb.Name, err) + return nil, err + } + os.waitLoadBalancerStatus(loadbalancer.ID) + } + + // create load balancer members. + members, err := os.getMembersByPoolID(pool.ID) + if err != nil && !isNotFound(err) { + return nil, fmt.Errorf("error getting members for pool %q: %v", pool.ID, err) + } + for _, ep := range lb.Endpoints { + if !memberExists(members, ep.Address, ep.Port) { + memberName := fmt.Sprintf("%s-%s-%d", lb.Name, ep.Address, ep.Port) + _, err = pools.CreateMember(os.Network, pool.ID, pools.CreateMemberOpts{ + Name: memberName, + ProtocolPort: ep.Port, + Address: ep.Address, + SubnetID: lb.SubnetID, + }).Extract() + if err != nil { + glog.Errorf("Create member %q failed: %v", memberName, err) + return nil, err + } + os.waitLoadBalancerStatus(loadbalancer.ID) + } else { + members = popMember(members, ep.Address, ep.Port) + } + } + // delete obsolete members + for _, member := range members { + glog.V(4).Infof("Deleting obsolete member %s for pool %s address %s", member.ID, + pool.ID, member.Address) + err := pools.DeleteMember(os.Network, pool.ID, member.ID).ExtractErr() + if err != nil && !isNotFound(err) { + return nil, fmt.Errorf("error deleting member %s for pool %s address %s: %v", + member.ID, pool.ID, member.Address, err) + } + } + + // create loadbalancer monitor. + if pool.MonitorID == "" { + _, err = monitors.Create(os.Network, monitors.CreateOpts{ + Name: lb.Name, + Type: monitors.TypeTCP, + PoolID: pool.ID, + TenantID: lb.TenantID, + Delay: defaultMonitorDelay, + Timeout: defaultMonotorTimeout, + MaxRetries: defaultMonitorRetry, + }).Extract() + if err != nil { + glog.Errorf("Create monitor for pool %q failed: %v", pool.ID, err) + return nil, err + } + } + + // associate external IP for the vip. + fip, err := os.associateFloatingIP(lb.TenantID, loadbalancer.VipPortID, lb.ExternalIP) + if err != nil { + glog.Errorf("associateFloatingIP for port %q failed: %v", loadbalancer.VipPortID, err) + return nil, err + } + + return &LoadBalancerStatus{ + InternalIP: loadbalancer.VipAddress, + ExternalIP: fip, + }, nil +} + +// GetLoadBalancer gets a load balancer by name. +func (os *Client) GetLoadBalancer(name string) (*LoadBalancer, error) { + // get load balancer + lb, err := os.getLoadBalanceByName(name) + if err != nil { + return nil, err + } + + // get listener + listener, err := os.getListenerByName(name) + if err != nil { + return nil, err + } + + // get members + endpoints := make([]Endpoint, 0) + for _, pool := range listener.Pools { + for _, m := range pool.Members { + endpoints = append(endpoints, Endpoint{ + Address: m.Address, + Port: m.ProtocolPort, + }) + } + } + + return &LoadBalancer{ + Name: lb.Name, + ServicePort: listener.ProtocolPort, + TenantID: lb.TenantID, + SubnetID: lb.VipSubnetID, + Protocol: listener.Protocol, + InternalIP: lb.VipAddress, + SessionAffinity: listener.Pools[0].Persistence.Type != "", + Endpoints: endpoints, + }, nil +} + +// LoadBalancerExist returns whether a load balancer has already been exist. +func (os *Client) LoadBalancerExist(name string) (bool, error) { + _, err := os.getLoadBalanceByName(name) + if err != nil { + if isNotFound(err) { + return false, nil + } + + return false, err + } + + return true, nil +} + +// EnsureLoadBalancerDeleted ensures a load balancer is deleted. +func (os *Client) EnsureLoadBalancerDeleted(name string) error { + // get load balancer + lb, err := os.getLoadBalanceByName(name) + if err != nil { + if isNotFound(err) { + return nil + } + + return err + } + + // delete floatingip + floatingIP, err := os.getFloatingIPByPortID(lb.VipPortID) + if err != nil && !isNotFound(err) { + return fmt.Errorf("error getting floating ip by port %q: %v", lb.VipPortID, err) + } + if floatingIP != nil { + err = floatingips.Delete(os.Network, floatingIP.ID).ExtractErr() + if err != nil && !isNotFound(err) { + return fmt.Errorf("error deleting floating ip %q: %v", floatingIP.ID, err) + } + } + + // get listeners and corelative pools and members + var poolIDs []string + var monitorIDs []string + var memberIDs []string + listenerList, err := os.getListenersByLoadBalancerID(lb.ID) + if err != nil { + return fmt.Errorf("Error getting load balancer %s listeners: %v", lb.ID, err) + } + + for _, listener := range listenerList { + pool, err := os.getPoolByListenerID(lb.ID, listener.ID) + if err != nil && !isNotFound(err) { + return fmt.Errorf("error getting pool for listener %s: %v", listener.ID, err) + } + poolIDs = append(poolIDs, pool.ID) + if pool.MonitorID != "" { + monitorIDs = append(monitorIDs, pool.MonitorID) + } + } + for _, pool := range poolIDs { + membersList, err := os.getMembersByPoolID(pool) + if err != nil && !isNotFound(err) { + return fmt.Errorf("Error getting pool members %s: %v", pool, err) + } + for _, member := range membersList { + memberIDs = append(memberIDs, member.ID) + } + } + + // delete all monitors + for _, monitorID := range monitorIDs { + err := monitors.Delete(os.Network, monitorID).ExtractErr() + if err != nil && !isNotFound(err) { + return err + } + os.waitLoadBalancerStatus(lb.ID) + } + + // delete all members and pools + for _, poolID := range poolIDs { + // delete all members for this pool + for _, memberID := range memberIDs { + err := pools.DeleteMember(os.Network, poolID, memberID).ExtractErr() + if err != nil && !isNotFound(err) { + return err + } + os.waitLoadBalancerStatus(lb.ID) + } + + // delete pool + err := pools.Delete(os.Network, poolID).ExtractErr() + if err != nil && !isNotFound(err) { + return err + } + os.waitLoadBalancerStatus(lb.ID) + } + + // delete all listeners + for _, listener := range listenerList { + err := listeners.Delete(os.Network, listener.ID).ExtractErr() + if err != nil && !isNotFound(err) { + return err + } + os.waitLoadBalancerStatus(lb.ID) + } + + // delete the load balancer + err = loadbalancers.Delete(os.Network, lb.ID).ExtractErr() + if err != nil && !isNotFound(err) { + return err + } + os.waitLoadBalancerStatus(lb.ID) + + return nil +} + +func (os *Client) ensureListenerDeleted(loadbalancerID string, listener listeners.Listener) error { + for _, pool := range listener.Pools { + for _, member := range pool.Members { + // delete member + if err := pools.DeleteMember(os.Network, pool.ID, member.ID).ExtractErr(); err != nil && !isNotFound(err) { + return err + } + os.waitLoadBalancerStatus(loadbalancerID) + } + + // delete monitor + if err := monitors.Delete(os.Network, pool.MonitorID).ExtractErr(); err != nil && !isNotFound(err) { + return err + } + os.waitLoadBalancerStatus(loadbalancerID) + + // delete pool + if err := pools.Delete(os.Network, pool.ID).ExtractErr(); err != nil && !isNotFound(err) { + return err + } + os.waitLoadBalancerStatus(loadbalancerID) + } + + // delete listener + if err := listeners.Delete(os.Network, listener.ID).ExtractErr(); err != nil && !isNotFound(err) { + return err + } + os.waitLoadBalancerStatus(loadbalancerID) + + return nil +} + +func (os *Client) waitLoadBalancerStatus(loadbalancerID string) (string, error) { + backoff := wait.Backoff{ + Duration: loadbalancerActiveInitDealy, + Factor: loadbalancerActiveFactor, + Steps: loadbalancerActiveSteps, + } + + var provisioningStatus string + err := wait.ExponentialBackoff(backoff, func() (bool, error) { + loadbalancer, err := loadbalancers.Get(os.Network, loadbalancerID).Extract() + if err != nil { + return false, err + } + provisioningStatus = loadbalancer.ProvisioningStatus + if loadbalancer.ProvisioningStatus == activeStatus { + return true, nil + } else if loadbalancer.ProvisioningStatus == errorStatus { + return true, fmt.Errorf("Loadbalancer has gone into ERROR state") + } else { + return false, nil + } + + }) + + if err == wait.ErrWaitTimeout { + err = fmt.Errorf("Loadbalancer failed to go into ACTIVE provisioning status within alloted time") + } + return provisioningStatus, err +} + +func (os *Client) waitLoadbalancerDeleted(loadbalancerID string) error { + backoff := wait.Backoff{ + Duration: loadbalancerDeleteInitDealy, + Factor: loadbalancerDeleteFactor, + Steps: loadbalancerDeleteSteps, + } + err := wait.ExponentialBackoff(backoff, func() (bool, error) { + _, err := loadbalancers.Get(os.Network, loadbalancerID).Extract() + if err != nil { + if err == ErrNotFound { + return true, nil + } else { + return false, err + } + } else { + return false, nil + } + }) + + if err == wait.ErrWaitTimeout { + err = fmt.Errorf("Loadbalancer failed to delete within the alloted time") + } + + return err +} + +func (os *Client) getListenersByLoadBalancerID(id string) ([]listeners.Listener, error) { + var existingListeners []listeners.Listener + err := listeners.List(os.Network, listeners.ListOpts{LoadbalancerID: id}).EachPage(func(page pagination.Page) (bool, error) { + listenerList, err := listeners.ExtractListeners(page) + if err != nil { + return false, err + } + for _, l := range listenerList { + for _, lb := range l.Loadbalancers { + if lb.ID == id { + existingListeners = append(existingListeners, l) + break + } + } + } + + return true, nil + }) + if err != nil { + return nil, err + } + + return existingListeners, nil +} + +func (os *Client) getLoadBalanceByName(name string) (*loadbalancers.LoadBalancer, error) { + var lb *loadbalancers.LoadBalancer + + opts := loadbalancers.ListOpts{Name: name} + pager := loadbalancers.List(os.Network, opts) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + lbs, err := loadbalancers.ExtractLoadBalancers(page) + if err != nil { + return false, err + } + + switch len(lbs) { + case 0: + return false, ErrNotFound + case 1: + lb = &lbs[0] + return true, nil + default: + return false, ErrMultipleResults + } + }) + if err != nil { + return nil, err + } + + if lb == nil { + return nil, ErrNotFound + } + + return lb, nil +} + +func (os *Client) getPoolByListenerID(loadbalancerID string, listenerID string) (*pools.Pool, error) { + listenerPools := make([]pools.Pool, 0, 1) + err := pools.List(os.Network, pools.ListOpts{LoadbalancerID: loadbalancerID}).EachPage( + func(page pagination.Page) (bool, error) { + poolsList, err := pools.ExtractPools(page) + if err != nil { + return false, err + } + for _, p := range poolsList { + for _, l := range p.Listeners { + if l.ID == listenerID { + listenerPools = append(listenerPools, p) + } + } + } + if len(listenerPools) > 1 { + return false, ErrMultipleResults + } + return true, nil + }) + if err != nil { + if isNotFound(err) { + return nil, ErrNotFound + } + return nil, err + } + + if len(listenerPools) == 0 { + return nil, ErrNotFound + } else if len(listenerPools) > 1 { + return nil, ErrMultipleResults + } + + return &listenerPools[0], nil +} + +// getPoolByName gets openstack pool by name. +func (os *Client) getPoolByName(name string) (*pools.Pool, error) { + var pool *pools.Pool + + opts := pools.ListOpts{Name: name} + pager := pools.List(os.Network, opts) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + ps, err := pools.ExtractPools(page) + if err != nil { + return false, err + } + + switch len(ps) { + case 0: + return false, ErrNotFound + case 1: + pool = &ps[0] + return true, nil + default: + return false, ErrMultipleResults + } + }) + if err != nil { + return nil, err + } + + if pool == nil { + return nil, ErrNotFound + } + + return pool, nil +} + +func (os *Client) getListenerByName(name string) (*listeners.Listener, error) { + var listener *listeners.Listener + + opts := listeners.ListOpts{Name: name} + pager := listeners.List(os.Network, opts) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + lists, err := listeners.ExtractListeners(page) + if err != nil { + return false, err + } + + switch len(lists) { + case 0: + return false, ErrNotFound + case 1: + listener = &lists[0] + return true, nil + default: + return false, ErrMultipleResults + } + }) + if err != nil { + return nil, err + } + + if listener == nil { + return nil, ErrNotFound + } + + return listener, nil +} + +func (os *Client) getMembersByPoolID(id string) ([]pools.Member, error) { + var members []pools.Member + err := pools.ListMembers(os.Network, id, pools.ListMembersOpts{}).EachPage(func(page pagination.Page) (bool, error) { + membersList, err := pools.ExtractMembers(page) + if err != nil { + return false, err + } + members = append(members, membersList...) + + return true, nil + }) + if err != nil { + return nil, err + } + + return members, nil +} + +func (os *Client) getFloatingIPByPortID(portID string) (*floatingips.FloatingIP, error) { + opts := floatingips.ListOpts{ + PortID: portID, + } + pager := floatingips.List(os.Network, opts) + + floatingIPList := make([]floatingips.FloatingIP, 0, 1) + + err := pager.EachPage(func(page pagination.Page) (bool, error) { + f, err := floatingips.ExtractFloatingIPs(page) + if err != nil { + return false, err + } + floatingIPList = append(floatingIPList, f...) + if len(floatingIPList) > 1 { + return false, ErrMultipleResults + } + return true, nil + }) + if err != nil { + if isNotFound(err) { + return nil, ErrNotFound + } + return nil, err + } + + if len(floatingIPList) == 0 { + return nil, ErrNotFound + } else if len(floatingIPList) > 1 { + return nil, ErrMultipleResults + } + + return &floatingIPList[0], nil +} + +// Check if a member exists for node +func memberExists(members []pools.Member, addr string, port int) bool { + for _, member := range members { + if member.Address == addr && member.ProtocolPort == port { + return true + } + } + + return false +} + +func (os *Client) associateFloatingIP(tenantID, portID, floatingIPAddress string) (string, error) { + var fip *floatingips.FloatingIP + opts := floatingips.ListOpts{FloatingIP: floatingIPAddress} + pager := floatingips.List(os.Network, opts) + err := pager.EachPage(func(page pagination.Page) (bool, error) { + floatingipList, err := floatingips.ExtractFloatingIPs(page) + if err != nil { + return false, err + } + + if len(floatingipList) > 0 { + fip = &floatingipList[0] + } + + return true, nil + }) + if err != nil { + return "", err + } + + if fip != nil { + if fip.PortID != "" { + if fip.PortID == portID { + glog.V(3).Infof("FIP %q has already been associated with port %q", floatingIPAddress, portID) + return fip.FloatingIP, nil + } + // fip has already been used + return fip.FloatingIP, fmt.Errorf("FloatingIP %v is already been binded to %v", floatingIPAddress, fip.PortID) + } + + // Update floatingip + floatOpts := floatingips.UpdateOpts{PortID: &portID} + _, err = floatingips.Update(os.Network, fip.ID, floatOpts).Extract() + if err != nil { + glog.Errorf("Bind floatingip %v to %v failed: %v", floatingIPAddress, portID, err) + return "", err + } + } else { + // Create floatingip + opts := floatingips.CreateOpts{ + FloatingNetworkID: os.ExtNetID, + TenantID: tenantID, + FloatingIP: floatingIPAddress, + PortID: portID, + } + fip, err = floatingips.Create(os.Network, opts).Extract() + if err != nil { + glog.Errorf("Create floatingip failed: %v", err) + return "", err + } + } + + return fip.FloatingIP, nil +} + +func popMember(members []pools.Member, addr string, port int) []pools.Member { + for i, member := range members { + if member.Address == addr && member.ProtocolPort == port { + members[i] = members[len(members)-1] + members = members[:len(members)-1] + } + } + + return members +} + +func isNotFound(err error) bool { + if err == ErrNotFound { + return true + } + + if _, ok := err.(*gophercloud.ErrResourceNotFound); ok { + return true + } + + if _, ok := err.(*gophercloud.ErrDefault404); ok { + return true + } + + return false +} diff --git a/pkg/proxy/helpers.go b/pkg/proxy/helpers.go index f9ec750..680b882 100644 --- a/pkg/proxy/helpers.go +++ b/pkg/proxy/helpers.go @@ -197,15 +197,6 @@ func getServiceHealthCheckNodePort(service *v1.Service) int32 { return service.Spec.HealthCheckNodePort } -func loadBalancerStatusDeepCopy(lb *v1.LoadBalancerStatus) *v1.LoadBalancerStatus { - c := &v1.LoadBalancerStatus{} - c.Ingress = make([]v1.LoadBalancerIngress, len(lb.Ingress)) - for i := range lb.Ingress { - c.Ingress[i] = lb.Ingress[i] - } - return c -} - func getRouterNetns(routerID string) string { return "qrouter-" + routerID } diff --git a/pkg/proxy/proxier.go b/pkg/proxy/proxier.go index e2677a0..debe664 100644 --- a/pkg/proxy/proxier.go +++ b/pkg/proxy/proxier.go @@ -24,10 +24,10 @@ import ( ) const ( - defaultSyncPerios = 15 * time.Minute - minSyncPeriod = 5 * time.Second - syncPeriod = 30 * time.Second - burstSyncs = 2 + defaultResyncPeriod = 15 * time.Minute + minSyncPeriod = 5 * time.Second + syncPeriod = 30 * time.Second + burstSyncs = 2 ) type Proxier struct { @@ -81,7 +81,7 @@ func NewProxier(kubeConfig, openstackConfig string) (*Proxier, error) { return nil, fmt.Errorf("failed to build clientset: %v", err) } - factory := informers.NewSharedInformerFactory(clientset, defaultSyncPerios) + factory := informers.NewSharedInformerFactory(clientset, defaultResyncPeriod) proxier := &Proxier{ kubeClientset: clientset, osClient: osClient, @@ -488,7 +488,7 @@ func (p *Proxier) syncProxyRules() { // Step 3: compose iptables chain. netns := getRouterNetns(nsInfo.router) if !netnsExist(netns) { - glog.V(3).Infof("Netns %q doesn't exist, omit the service") + glog.V(3).Infof("Netns %q doesn't exist, omit the services in namespace %q", netns, namespace) continue } @@ -522,8 +522,8 @@ func (p *Proxier) syncProxyRules() { svcNameString := svcInfo.serviceNameString // Step 5.1: check service type. - // Only ClusterIP service is supported now. - // TODO: support other types of services. + // Only ClusterIP service is supported. NodePort service is not supported since networks are L2 isolated. + // LoadBalancer service is handled in service controller. if svcInfo.serviceType != v1.ServiceTypeClusterIP { glog.V(3).Infof("Only ClusterIP service is supported, omitting service %q", svcName.NamespacedName) continue diff --git a/pkg/proxy/types.go b/pkg/proxy/types.go index 9128190..a096dd3 100644 --- a/pkg/proxy/types.go +++ b/pkg/proxy/types.go @@ -9,6 +9,7 @@ import ( "github.com/golang/glog" + "git.openstack.org/openstack/stackube/pkg/util" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" @@ -93,7 +94,7 @@ func newServiceInfo(svcPortName servicePortName, port *v1.ServicePort, service * nodePort: int(port.NodePort), serviceType: service.Spec.Type, // Deep-copy in case the service instance changes - loadBalancerStatus: *loadBalancerStatusDeepCopy(&service.Status.LoadBalancer), + loadBalancerStatus: *util.LoadBalancerStatusDeepCopy(&service.Status.LoadBalancer), sessionAffinityType: service.Spec.SessionAffinity, stickyMaxAgeMinutes: 180, externalIPs: make([]string, len(service.Spec.ExternalIPs)), diff --git a/pkg/service-controller/helpers.go b/pkg/service-controller/helpers.go new file mode 100644 index 0000000..767d824 --- /dev/null +++ b/pkg/service-controller/helpers.go @@ -0,0 +1,19 @@ +package service + +import ( + "fmt" + + "k8s.io/api/core/v1" +) + +const ( + lbPrefix = "stackube" +) + +func buildServiceName(service *v1.Service) string { + return fmt.Sprintf("%s_%s", service.Namespace, service.Name) +} + +func buildLoadBalancerName(service *v1.Service) string { + return fmt.Sprintf("%s_%s_%s", lbPrefix, service.Namespace, service.Name) +} diff --git a/pkg/service-controller/service_controller.go b/pkg/service-controller/service_controller.go new file mode 100644 index 0000000..da7a4d2 --- /dev/null +++ b/pkg/service-controller/service_controller.go @@ -0,0 +1,671 @@ +package service + +import ( + "fmt" + "reflect" + "sync" + "time" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/informers" + informersV1 "k8s.io/client-go/informers/core/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + + "git.openstack.org/openstack/stackube/pkg/openstack" + "git.openstack.org/openstack/stackube/pkg/util" + "github.com/golang/glog" +) + +const ( + // Interval of synchronizing service status from apiserver + serviceSyncPeriod = 30 * time.Second + resyncPeriod = 5 * time.Minute + + // How long to wait before retrying the processing of a service change. + minRetryDelay = 5 * time.Second + maxRetryDelay = 300 * time.Second + + clientRetryCount = 5 + clientRetryInterval = 5 * time.Second + concurrentServiceSyncs = 1 + retryable = true + notRetryable = false + + doNotRetry = time.Duration(0) +) + +type cachedService struct { + // The cached state of the service + state *v1.Service + // Controls error back-off + lastRetryDelay time.Duration +} + +type serviceCache struct { + mu sync.Mutex // protects serviceMap + serviceMap map[string]*cachedService +} + +type ServiceController struct { + cache *serviceCache + + kubeClientset *kubernetes.Clientset + osClient *openstack.Client + factory informers.SharedInformerFactory + serviceInformer informersV1.ServiceInformer + endpointInformer informersV1.EndpointsInformer + + // services that need to be synced + workingQueue workqueue.DelayingInterface +} + +// NewServiceController returns a new service controller to keep openstack lbaas resources +// (load balancers) in sync with the registry. +func NewServiceController(kubeClient *kubernetes.Clientset, + osClient *openstack.Client) (*ServiceController, error) { + factory := informers.NewSharedInformerFactory(kubeClient, resyncPeriod) + s := &ServiceController{ + osClient: osClient, + factory: factory, + kubeClientset: kubeClient, + cache: &serviceCache{serviceMap: make(map[string]*cachedService)}, + workingQueue: workqueue.NewNamedDelayingQueue("service"), + serviceInformer: factory.Core().V1().Services(), + endpointInformer: factory.Core().V1().Endpoints(), + } + + s.serviceInformer.Informer().AddEventHandlerWithResyncPeriod( + cache.ResourceEventHandlerFuncs{ + AddFunc: s.enqueueService, + UpdateFunc: func(old, cur interface{}) { + oldSvc, ok1 := old.(*v1.Service) + curSvc, ok2 := cur.(*v1.Service) + if ok1 && ok2 && s.needsUpdate(oldSvc, curSvc) { + s.enqueueService(cur) + } + }, + DeleteFunc: s.enqueueService, + }, + serviceSyncPeriod, + ) + + s.endpointInformer.Informer().AddEventHandlerWithResyncPeriod( + cache.ResourceEventHandlerFuncs{ + AddFunc: s.enqueueEndpoints, + UpdateFunc: func(old, cur interface{}) { + oldEndpoint, ok1 := old.(*v1.Endpoints) + curEndpoint, ok2 := cur.(*v1.Endpoints) + if ok1 && ok2 && s.needsUpdateEndpoint(oldEndpoint, curEndpoint) { + s.enqueueEndpoints(cur) + } + }, + DeleteFunc: s.enqueueEndpoints, + }, + serviceSyncPeriod, + ) + + return s, nil +} + +// obj could be an *v1.Service, or a DeletionFinalStateUnknown marker item. +func (s *ServiceController) enqueueService(obj interface{}) { + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + glog.Errorf("Couldn't get key for object %#v: %v", obj, err) + return + } + s.workingQueue.Add(key) +} + +// Run starts a background goroutine that watches for changes to services that +// have (or had) LoadBalancers=true and ensures that they have +// load balancers created and deleted appropriately. +// serviceSyncPeriod controls how often we check the cluster's services to +// ensure that the correct load balancers exist. +// +// It's an error to call Run() more than once for a given ServiceController +// object. +func (s *ServiceController) Run(stopCh <-chan struct{}) error { + defer runtime.HandleCrash() + defer s.workingQueue.ShutDown() + + glog.Info("Starting service controller") + defer glog.Info("Shutting down service controller") + + go s.factory.Start(stopCh) + + if !cache.WaitForCacheSync(stopCh, s.serviceInformer.Informer().HasSynced) { + return fmt.Errorf("failed to cache services") + } + if !cache.WaitForCacheSync(stopCh, s.endpointInformer.Informer().HasSynced) { + return fmt.Errorf("failed to cache endpoints") + } + + glog.Infof("Service informer cached") + + for i := 0; i < concurrentServiceSyncs; i++ { + go wait.Until(s.worker, time.Second, stopCh) + } + + <-stopCh + return nil +} + +// obj could be an *v1.Endpoint, or a DeletionFinalStateUnknown marker item. +func (s *ServiceController) enqueueEndpoints(obj interface{}) { + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + glog.Errorf("Couldn't get key for object %#v: %v", obj, err) + return + } + s.workingQueue.Add(key) +} + +// worker runs a worker thread that just dequeues items, processes them, and marks them done. +// It enforces that the syncHandler is never invoked concurrently with the same key. +func (s *ServiceController) worker() { + for { + func() { + key, quit := s.workingQueue.Get() + if quit { + return + } + defer s.workingQueue.Done(key) + err := s.syncService(key.(string)) + if err != nil { + glog.Errorf("Error syncing service: %v", err) + } + }() + } +} + +// Returns an error if processing the service update failed, along with a time.Duration +// indicating whether processing should be retried; zero means no-retry; otherwise +// we should retry in that Duration. +func (s *ServiceController) processServiceUpdate(cachedService *cachedService, service *v1.Service, + key string) (error, time.Duration) { + // cache the service, we need the info for service deletion + cachedService.state = service + err, retry := s.createLoadBalancerIfNeeded(key, service) + if err != nil { + message := "Error creating load balancer" + if retry { + message += " (will retry): " + } else { + message += " (will not retry): " + } + message += err.Error() + glog.V(3).Infof("Create service %q failed: %v, message: %q", buildServiceName(service), err, message) + + return err, cachedService.nextRetryDelay() + } + // Always update the cache upon success. + // NOTE: Since we update the cached service if and only if we successfully + // processed it, a cached service being nil implies that it hasn't yet + // been successfully processed. + s.cache.set(key, cachedService) + + cachedService.resetRetryDelay() + return nil, doNotRetry +} + +// Returns whatever error occurred along with a boolean indicator of whether it should be retried. +func (s *ServiceController) createLoadBalancerIfNeeded(key string, service *v1.Service) (error, bool) { + // Save the state so we can avoid a write if it doesn't change + previousState := util.LoadBalancerStatusDeepCopy(&service.Status.LoadBalancer) + var newState *v1.LoadBalancerStatus + var err error + + lbName := buildLoadBalancerName(service) + if !wantsLoadBalancer(service) { + needDelete := true + exists, err := s.osClient.LoadBalancerExist(lbName) + if err != nil { + return fmt.Errorf("Error getting LB for service %s: %v", key, err), retryable + } + if !exists { + needDelete = false + } + + if needDelete { + glog.Infof("Deleting existing load balancer for service %s that no longer needs a load balancer.", key) + if err := s.osClient.EnsureLoadBalancerDeleted(lbName); err != nil { + glog.Errorf("EnsureLoadBalancerDeleted %q failed: %v", lbName, err) + return err, retryable + } + } + + newState = &v1.LoadBalancerStatus{} + } else { + glog.V(2).Infof("Ensuring LB for service %s", key) + + // The load balancer doesn't exist yet, so create it. + newState, err = s.createLoadBalancer(service) + if err != nil { + return fmt.Errorf("Failed to create load balancer for service %s: %v", key, err), retryable + } + glog.V(3).Infof("LoadBalancer %q created", lbName) + } + + // Write the state if changed + // TODO: Be careful here ... what if there were other changes to the service? + if !util.LoadBalancerStatusEqual(previousState, newState) { + // Make a copy so we don't mutate the shared informer cache + copy, err := scheme.Scheme.DeepCopy(service) + if err != nil { + return err, retryable + } + service = copy.(*v1.Service) + + // Update the status on the copy + service.Status.LoadBalancer = *newState + + if err := s.persistUpdate(service); err != nil { + return fmt.Errorf("Failed to persist updated status to apiserver, even after retries. Giving up: %v", err), notRetryable + } + } else { + glog.V(2).Infof("Not persisting unchanged LoadBalancerStatus for service %s to kubernetes.", key) + } + + return nil, notRetryable +} + +func (s *ServiceController) persistUpdate(service *v1.Service) error { + var err error + for i := 0; i < clientRetryCount; i++ { + _, err = s.kubeClientset.CoreV1Client.Services(service.Namespace).UpdateStatus(service) + if err == nil { + return nil + } + // If the object no longer exists, we don't want to recreate it. Just bail + // out so that we can process the delete, which we should soon be receiving + // if we haven't already. + if errors.IsNotFound(err) { + glog.Infof("Not persisting update to service '%s/%s' that no longer exists: %v", + service.Namespace, service.Name, err) + return nil + } + // TODO: Try to resolve the conflict if the change was unrelated to load + // balancer status. For now, just pass it up the stack. + if errors.IsConflict(err) { + return fmt.Errorf("Not persisting update to service '%s/%s' that has been changed since we received it: %v", + service.Namespace, service.Name, err) + } + glog.Warningf("Failed to persist updated LoadBalancerStatus to service '%s/%s' after creating its load balancer: %v", + service.Namespace, service.Name, err) + time.Sleep(clientRetryInterval) + } + return err +} + +func (s *ServiceController) createLoadBalancer(service *v1.Service) (*v1.LoadBalancerStatus, error) { + // Only one protocol and one externalIPs supported per service. + if len(service.Spec.ExternalIPs) > 1 { + return nil, fmt.Errorf("multiple floatingips are not supported") + } + if len(service.Spec.Ports) > 1 { + return nil, fmt.Errorf("multiple floatingips are not supported") + } + + // Only support one network and network's name is same with namespace. + networkName := util.BuildNetworkName(service.Namespace, service.Namespace) + network, err := s.osClient.GetNetwork(networkName) + if err != nil { + glog.Errorf("Get network by name %q failed: %v", networkName, err) + return nil, err + } + + // get endpoints for the service. + endpoints, err := s.getEndpoints(service) + if err != nil { + glog.Errorf("Get endpoints for service %q failed: %v", buildServiceName(service), err) + return nil, err + } + + // create the loadbalancer. + lbName := buildLoadBalancerName(service) + svcPort := service.Spec.Ports[0] + externalIP := "" + if len(service.Spec.ExternalIPs) > 0 { + externalIP = service.Spec.ExternalIPs[0] + } + lb, err := s.osClient.EnsureLoadBalancer(&openstack.LoadBalancer{ + Name: lbName, + Endpoints: endpoints, + ServicePort: int(svcPort.Port), + TenantID: network.TenantID, + SubnetID: network.Subnets[0].Uid, + Protocol: string(svcPort.Protocol), + ExternalIP: externalIP, + SessionAffinity: service.Spec.SessionAffinity != v1.ServiceAffinityNone, + }) + if err != nil { + glog.Errorf("EnsureLoadBalancer %q failed: %v", lbName, err) + return nil, err + } + + return &v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{{IP: lb.ExternalIP}}, + }, nil + +} + +func (s *ServiceController) getEndpoints(service *v1.Service) ([]openstack.Endpoint, error) { + endpoints, err := s.kubeClientset.CoreV1Client.Endpoints(service.Namespace).Get(service.Name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + results := make([]openstack.Endpoint, 0) + for i := range endpoints.Subsets { + ep := endpoints.Subsets[i] + for _, ip := range ep.Addresses { + for _, port := range ep.Ports { + results = append(results, openstack.Endpoint{ + Address: ip.IP, + Port: int(port.Port), + }) + } + } + } + + return results, nil +} + +// ListKeys implements the interface required by DeltaFIFO to list the keys we +// already know about. +func (s *serviceCache) ListKeys() []string { + s.mu.Lock() + defer s.mu.Unlock() + keys := make([]string, 0, len(s.serviceMap)) + for k := range s.serviceMap { + keys = append(keys, k) + } + return keys +} + +// GetByKey returns the value stored in the serviceMap under the given key +func (s *serviceCache) GetByKey(key string) (interface{}, bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + if v, ok := s.serviceMap[key]; ok { + return v, true, nil + } + return nil, false, nil +} + +// ListKeys implements the interface required by DeltaFIFO to list the keys we +// already know about. +func (s *serviceCache) allServices() []*v1.Service { + s.mu.Lock() + defer s.mu.Unlock() + services := make([]*v1.Service, 0, len(s.serviceMap)) + for _, v := range s.serviceMap { + services = append(services, v.state) + } + return services +} + +func (s *serviceCache) get(serviceName string) (*cachedService, bool) { + s.mu.Lock() + defer s.mu.Unlock() + service, ok := s.serviceMap[serviceName] + return service, ok +} + +func (s *serviceCache) getOrCreate(serviceName string) *cachedService { + s.mu.Lock() + defer s.mu.Unlock() + service, ok := s.serviceMap[serviceName] + if !ok { + service = &cachedService{} + s.serviceMap[serviceName] = service + } + return service +} + +func (s *serviceCache) set(serviceName string, service *cachedService) { + s.mu.Lock() + defer s.mu.Unlock() + s.serviceMap[serviceName] = service +} + +func (s *serviceCache) delete(serviceName string) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.serviceMap, serviceName) +} + +func (s *ServiceController) needsUpdateEndpoint(oldEndpoint, newEndpoint *v1.Endpoints) bool { + if len(oldEndpoint.Subsets) != len(newEndpoint.Subsets) { + return true + } + + if !reflect.DeepEqual(oldEndpoint.Subsets, newEndpoint.Subsets) { + return true + } + + return false +} + +func (s *ServiceController) needsUpdate(oldService *v1.Service, newService *v1.Service) bool { + if !wantsLoadBalancer(oldService) && !wantsLoadBalancer(newService) { + return false + } + if wantsLoadBalancer(oldService) != wantsLoadBalancer(newService) { + glog.V(3).Infof("service %q's type changed to LoadBalancer", buildServiceName(newService)) + return true + } + + if wantsLoadBalancer(newService) && !reflect.DeepEqual(oldService.Spec.LoadBalancerSourceRanges, newService.Spec.LoadBalancerSourceRanges) { + glog.V(3).Infof("service %q's LoadBalancerSourceRanges changed: %v -> %v", buildServiceName(newService), + oldService.Spec.LoadBalancerSourceRanges, newService.Spec.LoadBalancerSourceRanges) + return true + } + + if !portsEqualForLB(oldService, newService) || oldService.Spec.SessionAffinity != newService.Spec.SessionAffinity { + return true + } + if !loadBalancerIPsAreEqual(oldService, newService) { + glog.V(3).Infof("service %q's LoadbalancerIP changed: %v -> %v", buildServiceName(newService), + oldService.Spec.LoadBalancerIP, newService.Spec.LoadBalancerIP) + return true + } + if len(oldService.Spec.ExternalIPs) != len(newService.Spec.ExternalIPs) { + glog.V(3).Infof("service %q's ExternalIPs changed: %v -> %v", buildServiceName(newService), + len(oldService.Spec.ExternalIPs), len(newService.Spec.ExternalIPs)) + return true + } + for i := range oldService.Spec.ExternalIPs { + if oldService.Spec.ExternalIPs[i] != newService.Spec.ExternalIPs[i] { + glog.V(3).Infof("service %q's ExternalIP added: %v", buildServiceName(newService), + newService.Spec.ExternalIPs[i]) + return true + } + } + if !reflect.DeepEqual(oldService.Annotations, newService.Annotations) { + return true + } + if oldService.UID != newService.UID { + glog.V(3).Infof("service %q's UID changed: %v -> %v", buildServiceName(newService), + oldService.UID, newService.UID) + return true + } + if oldService.Spec.ExternalTrafficPolicy != newService.Spec.ExternalTrafficPolicy { + glog.V(3).Infof("service %q's ExternalTrafficPolicy changed: %v -> %v", buildServiceName(newService), + oldService.Spec.ExternalTrafficPolicy, newService.Spec.ExternalTrafficPolicy) + return true + } + + return false +} + +func getPortsForLB(service *v1.Service) ([]*v1.ServicePort, error) { + var protocol v1.Protocol + + ports := []*v1.ServicePort{} + for i := range service.Spec.Ports { + sp := &service.Spec.Ports[i] + // The check on protocol was removed here. The cloud provider itself is now responsible for all protocol validation + ports = append(ports, sp) + if protocol == "" { + protocol = sp.Protocol + } else if protocol != sp.Protocol && wantsLoadBalancer(service) { + // TODO: Convert error messages to use event recorder + return nil, fmt.Errorf("mixed protocol external load balancers are not supported.") + } + } + return ports, nil +} + +func portsEqualForLB(x, y *v1.Service) bool { + xPorts, err := getPortsForLB(x) + if err != nil { + return false + } + yPorts, err := getPortsForLB(y) + if err != nil { + return false + } + return portSlicesEqualForLB(xPorts, yPorts) +} + +func portSlicesEqualForLB(x, y []*v1.ServicePort) bool { + if len(x) != len(y) { + return false + } + + for i := range x { + if !portEqualForLB(x[i], y[i]) { + return false + } + } + return true +} + +func portEqualForLB(x, y *v1.ServicePort) bool { + // TODO: Should we check name? (In theory, an LB could expose it) + if x.Name != y.Name { + return false + } + + if x.Protocol != y.Protocol { + return false + } + + if x.Port != y.Port { + return false + } + + // We don't check TargetPort; that is not relevant for load balancing + // TODO: Should we blank it out? Or just check it anyway? + + return true +} + +func wantsLoadBalancer(service *v1.Service) bool { + return service.Spec.Type == v1.ServiceTypeLoadBalancer +} + +func loadBalancerIPsAreEqual(oldService, newService *v1.Service) bool { + return oldService.Spec.LoadBalancerIP == newService.Spec.LoadBalancerIP +} + +// Computes the next retry, using exponential backoff +// mutex must be held. +func (s *cachedService) nextRetryDelay() time.Duration { + s.lastRetryDelay = s.lastRetryDelay * 2 + if s.lastRetryDelay < minRetryDelay { + s.lastRetryDelay = minRetryDelay + } + if s.lastRetryDelay > maxRetryDelay { + s.lastRetryDelay = maxRetryDelay + } + return s.lastRetryDelay +} + +// Resets the retry exponential backoff. mutex must be held. +func (s *cachedService) resetRetryDelay() { + s.lastRetryDelay = time.Duration(0) +} + +// syncService will sync the Service with the given key if it has had its expectations fulfilled, +// meaning it did not expect to see any more of its pods created or deleted. This function is not meant to be +// invoked concurrently with the same key. +func (s *ServiceController) syncService(key string) error { + startTime := time.Now() + var cachedService *cachedService + var retryDelay time.Duration + defer func() { + glog.V(4).Infof("Finished syncing service %q (%v)", key, time.Now().Sub(startTime)) + }() + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + return err + } + + // service holds the latest service info from apiserver + service, err := s.serviceInformer.Lister().Services(namespace).Get(name) + switch { + case errors.IsNotFound(err): + // service absence in store means watcher caught the deletion, ensure LB info is cleaned + glog.Infof("Service has been deleted %v", key) + err, retryDelay = s.processServiceDeletion(key) + case err != nil: + glog.Infof("Unable to retrieve service %v from store: %v", key, err) + s.workingQueue.Add(key) + return err + default: + cachedService = s.cache.getOrCreate(key) + err, retryDelay = s.processServiceUpdate(cachedService, service, key) + } + + if retryDelay != 0 { + // Add the failed service back to the queue so we'll retry it. + glog.Errorf("Failed to process service. Retrying in %s: %v", retryDelay, err) + go func(obj interface{}, delay time.Duration) { + // put back the service key to working queue, it is possible that more entries of the service + // were added into the queue during the delay, but it does not mess as when handling the retry, + // it always get the last service info from service store + s.workingQueue.AddAfter(obj, delay) + }(key, retryDelay) + } else if err != nil { + runtime.HandleError(fmt.Errorf("Failed to process service. Not retrying: %v", err)) + } + return nil +} + +// Returns an error if processing the service deletion failed, along with a time.Duration +// indicating whether processing should be retried; zero means no-retry; otherwise +// we should retry after that Duration. +func (s *ServiceController) processServiceDeletion(key string) (error, time.Duration) { + cachedService, ok := s.cache.get(key) + if !ok { + return fmt.Errorf("Service %s not in cache even though the watcher thought it was. Ignoring the deletion.", key), doNotRetry + } + service := cachedService.state + // delete load balancer info only if the service type is LoadBalancer + if !wantsLoadBalancer(service) { + return nil, doNotRetry + } + + lbName := buildLoadBalancerName(service) + err := s.osClient.EnsureLoadBalancerDeleted(lbName) + if err != nil { + glog.Errorf("Error deleting load balancer (will retry): %v", err) + return err, cachedService.nextRetryDelay() + } + glog.V(3).Infof("Loadbalancer %q deleted", lbName) + s.cache.delete(key) + + cachedService.resetRetryDelay() + return nil, doNotRetry +} diff --git a/pkg/util/k8s_util.go b/pkg/util/k8s_util.go index 85ce20f..d89b647 100644 --- a/pkg/util/k8s_util.go +++ b/pkg/util/k8s_util.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "k8s.io/api/core/v1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -67,3 +68,38 @@ func WaitForCRDReady(clientset apiextensionsclient.Interface, crdName string) er } return nil } + +func LoadBalancerStatusDeepCopy(lb *v1.LoadBalancerStatus) *v1.LoadBalancerStatus { + c := &v1.LoadBalancerStatus{} + c.Ingress = make([]v1.LoadBalancerIngress, len(lb.Ingress)) + for i := range lb.Ingress { + c.Ingress[i] = lb.Ingress[i] + } + return c +} + +func LoadBalancerStatusEqual(l, r *v1.LoadBalancerStatus) bool { + return ingressSliceEqual(l.Ingress, r.Ingress) +} + +func ingressSliceEqual(lhs, rhs []v1.LoadBalancerIngress) bool { + if len(lhs) != len(rhs) { + return false + } + for i := range lhs { + if !ingressEqual(&lhs[i], &rhs[i]) { + return false + } + } + return true +} + +func ingressEqual(lhs, rhs *v1.LoadBalancerIngress) bool { + if lhs.IP != rhs.IP { + return false + } + if lhs.Hostname != rhs.Hostname { + return false + } + return true +} diff --git a/pkg/util/util.go b/pkg/util/util.go index bdfb525..c9c30a4 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -2,6 +2,7 @@ package util import ( "errors" + apiv1 "k8s.io/api/core/v1" ) @@ -19,7 +20,8 @@ var ErrMultipleResults = errors.New("MultipleResults") func BuildNetworkName(namespace, name string) string { if IsSystemNamespace(namespace) { - namespace = SystemTenant + // All system namespaces shares same network. + return namePrefix + "-" + SystemTenant + "-" + SystemTenant } return namePrefix + "-" + namespace + "-" + name } diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go new file mode 100644 index 0000000..8393087 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go @@ -0,0 +1,146 @@ +package floatingips + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + FloatingNetworkID string `q:"floating_network_id"` + PortID string `q:"port_id"` + FixedIP string `q:"fixed_ip_address"` + FloatingIP string `q:"floating_ip_address"` + TenantID string `q:"tenant_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + RouterID string `q:"router_id"` +} + +// List returns a Pager which allows you to iterate over a collection of +// floating IP resources. It accepts a ListOpts struct, which allows you to +// filter and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return FloatingIPPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder is the interface type must satisfy to be used as Create +// options. +type CreateOptsBuilder interface { + ToFloatingIPCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new floating IP +// resource. The only required fields are FloatingNetworkID and PortID which +// refer to the external network and internal port respectively. +type CreateOpts struct { + FloatingNetworkID string `json:"floating_network_id" required:"true"` + FloatingIP string `json:"floating_ip_address,omitempty"` + PortID string `json:"port_id,omitempty"` + FixedIP string `json:"fixed_ip_address,omitempty"` + TenantID string `json:"tenant_id,omitempty"` +} + +// ToFloatingIPCreateMap allows CreateOpts to satisfy the CreateOptsBuilder +// interface +func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "floatingip") +} + +// Create accepts a CreateOpts struct and uses the values provided to create a +// new floating IP resource. You can create floating IPs on external networks +// only. If you provide a FloatingNetworkID which refers to a network that is +// not external (i.e. its `router:external' attribute is False), the operation +// will fail and return a 400 error. +// +// If you do not specify a FloatingIP address value, the operation will +// automatically allocate an available address for the new resource. If you do +// choose to specify one, it must fall within the subnet range for the external +// network - otherwise the operation returns a 400 error. If the FloatingIP +// address is already in use, the operation returns a 409 error code. +// +// You can associate the new resource with an internal port by using the PortID +// field. If you specify a PortID that is not valid, the operation will fail and +// return 404 error code. +// +// You must also configure an IP address for the port associated with the PortID +// you have provided - this is what the FixedIP refers to: an IP fixed to a port. +// Because a port might be associated with multiple IP addresses, you can use +// the FixedIP field to associate a particular IP address rather than have the +// API assume for you. If you specify an IP address that is not valid, the +// operation will fail and return a 400 error code. If the PortID and FixedIP +// are already associated with another resource, the operation will fail and +// returns a 409 error code. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToFloatingIPCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular floating IP resource based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface type must satisfy to be used as Update +// options. +type UpdateOptsBuilder interface { + ToFloatingIPUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the values used when updating a floating IP resource. The +// only value that can be updated is which internal port the floating IP is +// linked to. To associate the floating IP with a new internal port, provide its +// ID. To disassociate the floating IP from all ports, provide an empty string. +type UpdateOpts struct { + PortID *string `json:"port_id"` +} + +// ToFloatingIPUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder +// interface +func (opts UpdateOpts) ToFloatingIPUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "floatingip") +} + +// Update allows floating IP resources to be updated. Currently, the only way to +// "update" a floating IP is to associate it with a new internal port, or +// disassociated it from all ports. See UpdateOpts for instructions of how to +// do this. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToFloatingIPUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete will permanently delete a particular floating IP resource. Please +// ensure this is what you want - you can also disassociate the IP from existing +// internal ports. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go new file mode 100644 index 0000000..29d5b56 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go @@ -0,0 +1,110 @@ +package floatingips + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// FloatingIP represents a floating IP resource. A floating IP is an external +// IP address that is mapped to an internal port and, optionally, a specific +// IP address on a private network. In other words, it enables access to an +// instance on a private network from an external network. For this reason, +// floating IPs can only be defined on networks where the `router:external' +// attribute (provided by the external network extension) is set to True. +type FloatingIP struct { + // Unique identifier for the floating IP instance. + ID string `json:"id"` + + // UUID of the external network where the floating IP is to be created. + FloatingNetworkID string `json:"floating_network_id"` + + // Address of the floating IP on the external network. + FloatingIP string `json:"floating_ip_address"` + + // UUID of the port on an internal network that is associated with the floating IP. + PortID string `json:"port_id"` + + // The specific IP address of the internal port which should be associated + // with the floating IP. + FixedIP string `json:"fixed_ip_address"` + + // Owner of the floating IP. Only admin users can specify a tenant identifier + // other than its own. + TenantID string `json:"tenant_id"` + + // The condition of the API resource. + Status string `json:"status"` + + //The ID of the router used for this Floating-IP + RouterID string `json:"router_id"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract a result and extracts a FloatingIP resource. +func (r commonResult) Extract() (*FloatingIP, error) { + var s struct { + FloatingIP *FloatingIP `json:"floatingip"` + } + err := r.ExtractInto(&s) + return s.FloatingIP, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of an update operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// FloatingIPPage is the page returned by a pager when traversing over a +// collection of floating IPs. +type FloatingIPPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of floating IPs has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r FloatingIPPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"floatingips_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a NetworkPage struct is empty. +func (r FloatingIPPage) IsEmpty() (bool, error) { + is, err := ExtractFloatingIPs(r) + return len(is) == 0, err +} + +// ExtractFloatingIPs accepts a Page struct, specifically a FloatingIPPage struct, +// and extracts the elements into a slice of FloatingIP structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractFloatingIPs(r pagination.Page) ([]FloatingIP, error) { + var s struct { + FloatingIPs []FloatingIP `json:"floatingips"` + } + err := (r.(FloatingIPPage)).ExtractInto(&s) + return s.FloatingIPs, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go new file mode 100644 index 0000000..1318a18 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go @@ -0,0 +1,13 @@ +package floatingips + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "floatingips" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go new file mode 100644 index 0000000..4a78447 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go @@ -0,0 +1,182 @@ +package listeners + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type Protocol string + +// Supported attributes for create/update operations. +const ( + ProtocolTCP Protocol = "TCP" + ProtocolHTTP Protocol = "HTTP" + ProtocolHTTPS Protocol = "HTTPS" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToListenerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular listener attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + LoadbalancerID string `q:"loadbalancer_id"` + DefaultPoolID string `q:"default_pool_id"` + Protocol string `q:"protocol"` + ProtocolPort int `q:"protocol_port"` + ConnectionLimit int `q:"connection_limit"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToListenerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToListenerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// routers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those routers that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToListenerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return ListenerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToListenerCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // The load balancer on which to provision this listener. + LoadbalancerID string `json:"loadbalancer_id" required:"true"` + // The protocol - can either be TCP, HTTP or HTTPS. + Protocol Protocol `json:"protocol" required:"true"` + // The port on which to listen for client traffic. + ProtocolPort int `json:"protocol_port" required:"true"` + // Indicates the owner of the Listener. Required for admins. + TenantID string `json:"tenant_id,omitempty"` + // Human-readable name for the Listener. Does not have to be unique. + Name string `json:"name,omitempty"` + // The ID of the default pool with which the Listener is associated. + DefaultPoolID string `json:"default_pool_id,omitempty"` + // Human-readable description for the Listener. + Description string `json:"description,omitempty"` + // The maximum number of connections allowed for the Listener. + ConnLimit *int `json:"connection_limit,omitempty"` + // A reference to a container of TLS secrets. + DefaultTlsContainerRef string `json:"default_tls_container_ref,omitempty"` + // A list of references to TLS secrets. + SniContainerRefs []string `json:"sni_container_refs,omitempty"` + // The administrative state of the Listener. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToListenerCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToListenerCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "listener") +} + +// Create is an operation which provisions a new Listeners based on the +// configuration defined in the CreateOpts struct. Once the request is +// validated and progress has started on the provisioning process, a +// CreateResult will be returned. +// +// Users with an admin role can create Listeners on behalf of other tenants by +// specifying a TenantID attribute different than their own. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToListenerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular Listeners based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToListenerUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Human-readable name for the Listener. Does not have to be unique. + Name string `json:"name,omitempty"` + // Human-readable description for the Listener. + Description string `json:"description,omitempty"` + // The maximum number of connections allowed for the Listener. + ConnLimit *int `json:"connection_limit,omitempty"` + // A reference to a container of TLS secrets. + DefaultTlsContainerRef string `json:"default_tls_container_ref,omitempty"` + // A list of references to TLS secrets. + SniContainerRefs []string `json:"sni_container_refs,omitempty"` + // The administrative state of the Listener. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToListenerUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToListenerUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "listener") +} + +// Update is an operation which modifies the attributes of the specified Listener. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) { + b, err := opts.ToListenerUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete will permanently delete a particular Listeners based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/results.go new file mode 100644 index 0000000..aa8ed1b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/results.go @@ -0,0 +1,114 @@ +package listeners + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools" + "github.com/gophercloud/gophercloud/pagination" +) + +type LoadBalancerID struct { + ID string `json:"id"` +} + +// Listener is the primary load balancing configuration object that specifies +// the loadbalancer and port on which client traffic is received, as well +// as other details such as the load balancing method to be use, protocol, etc. +type Listener struct { + // The unique ID for the Listener. + ID string `json:"id"` + // Owner of the Listener. Only an admin user can specify a tenant ID other than its own. + TenantID string `json:"tenant_id"` + // Human-readable name for the Listener. Does not have to be unique. + Name string `json:"name"` + // Human-readable description for the Listener. + Description string `json:"description"` + // The protocol to loadbalance. A valid value is TCP, HTTP, or HTTPS. + Protocol string `json:"protocol"` + // The port on which to listen to client traffic that is associated with the + // Loadbalancer. A valid value is from 0 to 65535. + ProtocolPort int `json:"protocol_port"` + // The UUID of default pool. Must have compatible protocol with listener. + DefaultPoolID string `json:"default_pool_id"` + // A list of load balancer IDs. + Loadbalancers []LoadBalancerID `json:"loadbalancers"` + // The maximum number of connections allowed for the Loadbalancer. Default is -1, + // meaning no limit. + ConnLimit int `json:"connection_limit"` + // The list of references to TLS secrets. + SniContainerRefs []string `json:"sni_container_refs"` + // Optional. A reference to a container of TLS secrets. + DefaultTlsContainerRef string `json:"default_tls_container_ref"` + // The administrative state of the Listener. A valid value is true (UP) or false (DOWN). + AdminStateUp bool `json:"admin_state_up"` + Pools []pools.Pool `json:"pools"` +} + +// ListenerPage is the page returned by a pager when traversing over a +// collection of routers. +type ListenerPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of routers has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r ListenerPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"listeners_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a RouterPage struct is empty. +func (r ListenerPage) IsEmpty() (bool, error) { + is, err := ExtractListeners(r) + return len(is) == 0, err +} + +// ExtractListeners accepts a Page struct, specifically a ListenerPage struct, +// and extracts the elements into a slice of Listener structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractListeners(r pagination.Page) ([]Listener, error) { + var s struct { + Listeners []Listener `json:"listeners"` + } + err := (r.(ListenerPage)).ExtractInto(&s) + return s.Listeners, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*Listener, error) { + var s struct { + Listener *Listener `json:"listener"` + } + err := r.ExtractInto(&s) + return s.Listener, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/urls.go new file mode 100644 index 0000000..02fb1eb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/urls.go @@ -0,0 +1,16 @@ +package listeners + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "listeners" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go new file mode 100644 index 0000000..bc4a3c6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go @@ -0,0 +1,172 @@ +package loadbalancers + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToLoadBalancerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Loadbalancer attributes you want to see returned. SortKey allows you to +// sort by a particular attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Description string `q:"description"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + ProvisioningStatus string `q:"provisioning_status"` + VipAddress string `q:"vip_address"` + VipPortID string `q:"vip_port_id"` + VipSubnetID string `q:"vip_subnet_id"` + ID string `q:"id"` + OperatingStatus string `q:"operating_status"` + Name string `q:"name"` + Flavor string `q:"flavor"` + Provider string `q:"provider"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToLoadbalancerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToLoadBalancerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// routers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those routers that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToLoadBalancerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return LoadBalancerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToLoadBalancerCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Optional. Human-readable name for the Loadbalancer. Does not have to be unique. + Name string `json:"name,omitempty"` + // Optional. Human-readable description for the Loadbalancer. + Description string `json:"description,omitempty"` + // Required. The network on which to allocate the Loadbalancer's address. A tenant can + // only create Loadbalancers on networks authorized by policy (e.g. networks that + // belong to them or networks that are shared). + VipSubnetID string `json:"vip_subnet_id" required:"true"` + // Required for admins. The UUID of the tenant who owns the Loadbalancer. + // Only administrative users can specify a tenant UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + // Optional. The IP address of the Loadbalancer. + VipAddress string `json:"vip_address,omitempty"` + // Optional. The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + // Optional. The UUID of a flavor. + Flavor string `json:"flavor,omitempty"` + // Optional. The name of the provider. + Provider string `json:"provider,omitempty"` +} + +// ToLoadBalancerCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToLoadBalancerCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "loadbalancer") +} + +// Create is an operation which provisions a new loadbalancer based on the +// configuration defined in the CreateOpts struct. Once the request is +// validated and progress has started on the provisioning process, a +// CreateResult will be returned. +// +// Users with an admin role can create loadbalancers on behalf of other tenants by +// specifying a TenantID attribute different than their own. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToLoadBalancerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular Loadbalancer based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToLoadBalancerUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Optional. Human-readable name for the Loadbalancer. Does not have to be unique. + Name string `json:"name,omitempty"` + // Optional. Human-readable description for the Loadbalancer. + Description string `json:"description,omitempty"` + // Optional. The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToLoadBalancerUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToLoadBalancerUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "loadbalancer") +} + +// Update is an operation which modifies the attributes of the specified LoadBalancer. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) { + b, err := opts.ToLoadBalancerUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete will permanently delete a particular LoadBalancer based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} + +func GetStatuses(c *gophercloud.ServiceClient, id string) (r GetStatusesResult) { + _, r.Err = c.Get(statusRootURL(c, id), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go new file mode 100644 index 0000000..4423c24 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go @@ -0,0 +1,125 @@ +package loadbalancers + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners" + "github.com/gophercloud/gophercloud/pagination" +) + +// LoadBalancer is the primary load balancing configuration object that specifies +// the virtual IP address on which client traffic is received, as well +// as other details such as the load balancing method to be use, protocol, etc. +type LoadBalancer struct { + // Human-readable description for the Loadbalancer. + Description string `json:"description"` + // The administrative state of the Loadbalancer. A valid value is true (UP) or false (DOWN). + AdminStateUp bool `json:"admin_state_up"` + // Owner of the LoadBalancer. Only an admin user can specify a tenant ID other than its own. + TenantID string `json:"tenant_id"` + // The provisioning status of the LoadBalancer. This value is ACTIVE, PENDING_CREATE or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + // The IP address of the Loadbalancer. + VipAddress string `json:"vip_address"` + // The UUID of the port associated with the IP address. + VipPortID string `json:"vip_port_id"` + // The UUID of the subnet on which to allocate the virtual IP for the Loadbalancer address. + VipSubnetID string `json:"vip_subnet_id"` + // The unique ID for the LoadBalancer. + ID string `json:"id"` + // The operating status of the LoadBalancer. This value is ONLINE or OFFLINE. + OperatingStatus string `json:"operating_status"` + // Human-readable name for the LoadBalancer. Does not have to be unique. + Name string `json:"name"` + // The UUID of a flavor if set. + Flavor string `json:"flavor"` + // The name of the provider. + Provider string `json:"provider"` + Listeners []listeners.Listener `json:"listeners"` +} + +type StatusTree struct { + Loadbalancer *LoadBalancer `json:"loadbalancer"` +} + +// LoadBalancerPage is the page returned by a pager when traversing over a +// collection of routers. +type LoadBalancerPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of routers has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r LoadBalancerPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"loadbalancers_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a LoadBalancerPage struct is empty. +func (p LoadBalancerPage) IsEmpty() (bool, error) { + is, err := ExtractLoadBalancers(p) + return len(is) == 0, err +} + +// ExtractLoadBalancers accepts a Page struct, specifically a LoadbalancerPage struct, +// and extracts the elements into a slice of LoadBalancer structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractLoadBalancers(r pagination.Page) ([]LoadBalancer, error) { + var s struct { + LoadBalancers []LoadBalancer `json:"loadbalancers"` + } + err := (r.(LoadBalancerPage)).ExtractInto(&s) + return s.LoadBalancers, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*LoadBalancer, error) { + var s struct { + LoadBalancer *LoadBalancer `json:"loadbalancer"` + } + err := r.ExtractInto(&s) + return s.LoadBalancer, err +} + +type GetStatusesResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a Loadbalancer. +func (r GetStatusesResult) Extract() (*StatusTree, error) { + var s struct { + Statuses *StatusTree `json:"statuses"` + } + err := r.ExtractInto(&s) + return s.Statuses, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/urls.go new file mode 100644 index 0000000..73cf5dc --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/urls.go @@ -0,0 +1,21 @@ +package loadbalancers + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "loadbalancers" + statusPath = "statuses" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func statusRootURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, statusPath) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go new file mode 100644 index 0000000..1e776bf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go @@ -0,0 +1,233 @@ +package monitors + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToMonitorListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Monitor attributes you want to see returned. SortKey allows you to +// sort by a particular Monitor attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + TenantID string `q:"tenant_id"` + PoolID string `q:"pool_id"` + Type string `q:"type"` + Delay int `q:"delay"` + Timeout int `q:"timeout"` + MaxRetries int `q:"max_retries"` + HTTPMethod string `q:"http_method"` + URLPath string `q:"url_path"` + ExpectedCodes string `q:"expected_codes"` + AdminStateUp *bool `q:"admin_state_up"` + Status string `q:"status"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToMonitorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToMonitorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns a Pager which allows you to iterate over a collection of +// health monitors. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those health monitors that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToMonitorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return MonitorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Constants that represent approved monitoring types. +const ( + TypePING = "PING" + TypeTCP = "TCP" + TypeHTTP = "HTTP" + TypeHTTPS = "HTTPS" +) + +var ( + errDelayMustGETimeout = fmt.Errorf("Delay must be greater than or equal to timeout") +) + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToMonitorCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Required. The Pool to Monitor. + PoolID string `json:"pool_id" required:"true"` + // Required. The type of probe, which is PING, TCP, HTTP, or HTTPS, that is + // sent by the load balancer to verify the member state. + Type string `json:"type" required:"true"` + // Required. The time, in seconds, between sending probes to members. + Delay int `json:"delay" required:"true"` + // Required. Maximum number of seconds for a Monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int `json:"timeout" required:"true"` + // Required. Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int `json:"max_retries" required:"true"` + // Required for HTTP(S) types. URI path that will be accessed if Monitor type + // is HTTP or HTTPS. + URLPath string `json:"url_path,omitempty"` + // Required for HTTP(S) types. The HTTP method used for requests by the + // Monitor. If this attribute is not specified, it defaults to "GET". + HTTPMethod string `json:"http_method,omitempty"` + // Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S) + // Monitor. You can either specify a single status like "200", or a range + // like "200-202". + ExpectedCodes string `json:"expected_codes,omitempty"` + // Indicates the owner of the Loadbalancer. Required for admins. + TenantID string `json:"tenant_id,omitempty"` + // Optional. The Name of the Monitor. + Name string `json:"name,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToMonitorCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToMonitorCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "healthmonitor") + if err != nil { + return nil, err + } + + switch opts.Type { + case TypeHTTP, TypeHTTPS: + switch opts.URLPath { + case "": + return nil, fmt.Errorf("URLPath must be provided for HTTP and HTTPS") + } + switch opts.ExpectedCodes { + case "": + return nil, fmt.Errorf("ExpectedCodes must be provided for HTTP and HTTPS") + } + } + + return b, nil +} + +/* + Create is an operation which provisions a new Health Monitor. There are + different types of Monitor you can provision: PING, TCP or HTTP(S). Below + are examples of how to create each one. + + Here is an example config struct to use when creating a PING or TCP Monitor: + + CreateOpts{Type: TypePING, Delay: 20, Timeout: 10, MaxRetries: 3} + CreateOpts{Type: TypeTCP, Delay: 20, Timeout: 10, MaxRetries: 3} + + Here is an example config struct to use when creating a HTTP(S) Monitor: + + CreateOpts{Type: TypeHTTP, Delay: 20, Timeout: 10, MaxRetries: 3, + HttpMethod: "HEAD", ExpectedCodes: "200", PoolID: "2c946bfc-1804-43ab-a2ff-58f6a762b505"} +*/ +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToMonitorCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular Health Monitor based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToMonitorUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Required. The time, in seconds, between sending probes to members. + Delay int `json:"delay,omitempty"` + // Required. Maximum number of seconds for a Monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int `json:"timeout,omitempty"` + // Required. Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int `json:"max_retries,omitempty"` + // Required for HTTP(S) types. URI path that will be accessed if Monitor type + // is HTTP or HTTPS. + URLPath string `json:"url_path,omitempty"` + // Required for HTTP(S) types. The HTTP method used for requests by the + // Monitor. If this attribute is not specified, it defaults to "GET". + HTTPMethod string `json:"http_method,omitempty"` + // Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S) + // Monitor. You can either specify a single status like "200", or a range + // like "200-202". + ExpectedCodes string `json:"expected_codes,omitempty"` + // Optional. The Name of the Monitor. + Name string `json:"name,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToMonitorUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToMonitorUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "healthmonitor") +} + +// Update is an operation which modifies the attributes of the specified Monitor. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToMonitorUpdateMap() + if err != nil { + r.Err = err + return + } + + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + return +} + +// Delete will permanently delete a particular Monitor based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go new file mode 100644 index 0000000..05dcf47 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go @@ -0,0 +1,144 @@ +package monitors + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type PoolID struct { + ID string `json:"id"` +} + +// Monitor represents a load balancer health monitor. A health monitor is used +// to determine whether or not back-end members of the VIP's pool are usable +// for processing a request. A pool can have several health monitors associated +// with it. There are different types of health monitors supported: +// +// PING: used to ping the members using ICMP. +// TCP: used to connect to the members using TCP. +// HTTP: used to send an HTTP request to the member. +// HTTPS: used to send a secure HTTP request to the member. +// +// When a pool has several monitors associated with it, each member of the pool +// is monitored by all these monitors. If any monitor declares the member as +// unhealthy, then the member status is changed to INACTIVE and the member +// won't participate in its pool's load balancing. In other words, ALL monitors +// must declare the member to be healthy for it to stay ACTIVE. +type Monitor struct { + // The unique ID for the Monitor. + ID string `json:"id"` + + // The Name of the Monitor. + Name string `json:"name"` + + // Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id"` + + // The type of probe sent by the load balancer to verify the member state, + // which is PING, TCP, HTTP, or HTTPS. + Type string `json:"type"` + + // The time, in seconds, between sending probes to members. + Delay int `json:"delay"` + + // The maximum number of seconds for a monitor to wait for a connection to be + // established before it times out. This value must be less than the delay value. + Timeout int `json:"timeout"` + + // Number of allowed connection failures before changing the status of the + // member to INACTIVE. A valid value is from 1 to 10. + MaxRetries int `json:"max_retries"` + + // The HTTP method that the monitor uses for requests. + HTTPMethod string `json:"http_method"` + + // The HTTP path of the request sent by the monitor to test the health of a + // member. Must be a string beginning with a forward slash (/). + URLPath string `json:"url_path" ` + + // Expected HTTP codes for a passing HTTP(S) monitor. + ExpectedCodes string `json:"expected_codes"` + + // The administrative state of the health monitor, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + + // The status of the health monitor. Indicates whether the health monitor is + // operational. + Status string `json:"status"` + + // List of pools that are associated with the health monitor. + Pools []PoolID `json:"pools"` +} + +// MonitorPage is the page returned by a pager when traversing over a +// collection of health monitors. +type MonitorPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of monitors has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r MonitorPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"healthmonitors_links"` + } + + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a MonitorPage struct is empty. +func (r MonitorPage) IsEmpty() (bool, error) { + is, err := ExtractMonitors(r) + return len(is) == 0, err +} + +// ExtractMonitors accepts a Page struct, specifically a MonitorPage struct, +// and extracts the elements into a slice of Monitor structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMonitors(r pagination.Page) ([]Monitor, error) { + var s struct { + Monitors []Monitor `json:"healthmonitors"` + } + err := (r.(MonitorPage)).ExtractInto(&s) + return s.Monitors, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a monitor. +func (r commonResult) Extract() (*Monitor, error) { + var s struct { + Monitor *Monitor `json:"healthmonitor"` + } + err := r.ExtractInto(&s) + return s.Monitor, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go new file mode 100644 index 0000000..a222e52 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go @@ -0,0 +1,16 @@ +package monitors + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "healthmonitors" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go new file mode 100644 index 0000000..093df6a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go @@ -0,0 +1,334 @@ +package pools + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToPoolListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Pool attributes you want to see returned. SortKey allows you to +// sort by a particular Pool attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + LBMethod string `q:"lb_algorithm"` + Protocol string `q:"protocol"` + TenantID string `q:"tenant_id"` + AdminStateUp *bool `q:"admin_state_up"` + Name string `q:"name"` + ID string `q:"id"` + LoadbalancerID string `q:"loadbalancer_id"` + ListenerID string `q:"listener_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToPoolListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPoolListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// pools. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those pools that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToPoolListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PoolPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +type LBMethod string +type Protocol string + +// Supported attributes for create/update operations. +const ( + LBMethodRoundRobin LBMethod = "ROUND_ROBIN" + LBMethodLeastConnections LBMethod = "LEAST_CONNECTIONS" + LBMethodSourceIp LBMethod = "SOURCE_IP" + + ProtocolTCP Protocol = "TCP" + ProtocolHTTP Protocol = "HTTP" + ProtocolHTTPS Protocol = "HTTPS" +) + +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToPoolCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // The algorithm used to distribute load between the members of the pool. The + // current specification supports LBMethodRoundRobin, LBMethodLeastConnections + // and LBMethodSourceIp as valid values for this attribute. + LBMethod LBMethod `json:"lb_algorithm" required:"true"` + // The protocol used by the pool members, you can use either + // ProtocolTCP, ProtocolHTTP, or ProtocolHTTPS. + Protocol Protocol `json:"protocol" required:"true"` + // The Loadbalancer on which the members of the pool will be associated with. + // Note: one of LoadbalancerID or ListenerID must be provided. + LoadbalancerID string `json:"loadbalancer_id,omitempty" xor:"ListenerID"` + // The Listener on which the members of the pool will be associated with. + // Note: one of LoadbalancerID or ListenerID must be provided. + ListenerID string `json:"listener_id,omitempty" xor:"LoadbalancerID"` + // Only required if the caller has an admin role and wants to create a pool + // for another tenant. + TenantID string `json:"tenant_id,omitempty"` + // Name of the pool. + Name string `json:"name,omitempty"` + // Human-readable description for the pool. + Description string `json:"description,omitempty"` + // Omit this field to prevent session persistence. + Persistence *SessionPersistence `json:"session_persistence,omitempty"` + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToPoolCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToPoolCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "pool") +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// load balancer pool. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToPoolCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// Get retrieves a particular pool based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil) + return +} + +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToPoolUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Name of the pool. + Name string `json:"name,omitempty"` + // Human-readable description for the pool. + Description string `json:"description,omitempty"` + // The algorithm used to distribute load between the members of the pool. The + // current specification supports LBMethodRoundRobin, LBMethodLeastConnections + // and LBMethodSourceIp as valid values for this attribute. + LBMethod LBMethod `json:"lb_algorithm,omitempty"` + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToPoolUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToPoolUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "pool") +} + +// Update allows pools to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToPoolUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete will permanently delete a particular pool based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} + +// ListMemberOptsBuilder allows extensions to add additional parameters to the +// ListMembers request. +type ListMembersOptsBuilder interface { + ToMembersListQuery() (string, error) +} + +// ListMembersOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Member attributes you want to see returned. SortKey allows you to +// sort by a particular Member attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListMembersOpts struct { + Name string `q:"name"` + Weight int `q:"weight"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + Address string `q:"address"` + ProtocolPort int `q:"protocol_port"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToMemberListQuery formats a ListOpts into a query string. +func (opts ListMembersOpts) ToMembersListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListMembers returns a Pager which allows you to iterate over a collection of +// members. It accepts a ListMembersOptsBuilder, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those members that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func ListMembers(c *gophercloud.ServiceClient, poolID string, opts ListMembersOptsBuilder) pagination.Pager { + url := memberRootURL(c, poolID) + if opts != nil { + query, err := opts.ToMembersListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return MemberPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateMemberOptsBuilder is the interface options structs have to satisfy in order +// to be used in the CreateMember operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateMemberOptsBuilder interface { + ToMemberCreateMap() (map[string]interface{}, error) +} + +// CreateMemberOpts is the common options struct used in this package's CreateMember +// operation. +type CreateMemberOpts struct { + // Required. The IP address of the member to receive traffic from the load balancer. + Address string `json:"address" required:"true"` + // Required. The port on which to listen for client traffic. + ProtocolPort int `json:"protocol_port" required:"true"` + // Optional. Name of the Member. + Name string `json:"name,omitempty"` + // Only required if the caller has an admin role and wants to create a Member + // for another tenant. + TenantID string `json:"tenant_id,omitempty"` + // Optional. A positive integer value that indicates the relative portion of + // traffic that this member should receive from the pool. For example, a + // member with a weight of 10 receives five times as much traffic as a member + // with a weight of 2. + Weight int `json:"weight,omitempty"` + // Optional. If you omit this parameter, LBaaS uses the vip_subnet_id + // parameter value for the subnet UUID. + SubnetID string `json:"subnet_id,omitempty"` + // Optional. The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToMemberCreateMap casts a CreateOpts struct to a map. +func (opts CreateMemberOpts) ToMemberCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "member") +} + +// CreateMember will create and associate a Member with a particular Pool. +func CreateMember(c *gophercloud.ServiceClient, poolID string, opts CreateMemberOpts) (r CreateMemberResult) { + b, err := opts.ToMemberCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(memberRootURL(c, poolID), b, &r.Body, nil) + return +} + +// GetMember retrieves a particular Pool Member based on its unique ID. +func GetMember(c *gophercloud.ServiceClient, poolID string, memberID string) (r GetMemberResult) { + _, r.Err = c.Get(memberResourceURL(c, poolID, memberID), &r.Body, nil) + return +} + +// MemberUpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateMemberOptsBuilder interface { + ToMemberUpdateMap() (map[string]interface{}, error) +} + +// UpdateMemberOpts is the common options struct used in this package's Update +// operation. +type UpdateMemberOpts struct { + // Optional. Name of the Member. + Name string `json:"name,omitempty"` + // Optional. A positive integer value that indicates the relative portion of + // traffic that this member should receive from the pool. For example, a + // member with a weight of 10 receives five times as much traffic as a member + // with a weight of 2. + Weight int `json:"weight,omitempty"` + // Optional. The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToMemberUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateMemberOpts) ToMemberUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "member") +} + +// Update allows Member to be updated. +func UpdateMember(c *gophercloud.ServiceClient, poolID string, memberID string, opts UpdateMemberOptsBuilder) (r UpdateMemberResult) { + b, err := opts.ToMemberUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(memberResourceURL(c, poolID, memberID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + return +} + +// DisassociateMember will remove and disassociate a Member from a particular Pool. +func DeleteMember(c *gophercloud.ServiceClient, poolID string, memberID string) (r DeleteMemberResult) { + _, r.Err = c.Delete(memberResourceURL(c, poolID, memberID), nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/results.go new file mode 100644 index 0000000..0e0bf36 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/results.go @@ -0,0 +1,242 @@ +package pools + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors" + "github.com/gophercloud/gophercloud/pagination" +) + +// SessionPersistence represents the session persistence feature of the load +// balancing service. It attempts to force connections or requests in the same +// session to be processed by the same member as long as it is ative. Three +// types of persistence are supported: +// +// SOURCE_IP: With this mode, all connections originating from the same source +// IP address, will be handled by the same Member of the Pool. +// HTTP_COOKIE: With this persistence mode, the load balancing function will +// create a cookie on the first request from a client. Subsequent +// requests containing the same cookie value will be handled by +// the same Member of the Pool. +// APP_COOKIE: With this persistence mode, the load balancing function will +// rely on a cookie established by the backend application. All +// requests carrying the same cookie value will be handled by the +// same Member of the Pool. +type SessionPersistence struct { + // The type of persistence mode + Type string `json:"type"` + + // Name of cookie if persistence mode is set appropriately + CookieName string `json:"cookie_name,omitempty"` +} + +type LoadBalancerID struct { + ID string `json:"id"` +} + +type ListenerID struct { + ID string `json:"id"` +} + +// Pool represents a logical set of devices, such as web servers, that you +// group together to receive and process traffic. The load balancing function +// chooses a Member of the Pool according to the configured load balancing +// method to handle the new requests or connections received on the VIP address. +type Pool struct { + // The load-balancer algorithm, which is round-robin, least-connections, and + // so on. This value, which must be supported, is dependent on the provider. + // Round-robin must be supported. + LBMethod string `json:"lb_algorithm"` + // The protocol of the Pool, which is TCP, HTTP, or HTTPS. + Protocol string `json:"protocol"` + // Description for the Pool. + Description string `json:"description"` + // A list of listeners objects IDs. + Listeners []ListenerID `json:"listeners"` //[]map[string]interface{} + // A list of member objects IDs. + Members []Member `json:"members"` + // The ID of associated health monitor. + MonitorID string `json:"healthmonitor_id"` + // The network on which the members of the Pool will be located. Only members + // that are on this network can be added to the Pool. + SubnetID string `json:"subnet_id"` + // Owner of the Pool. Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id"` + // The administrative state of the Pool, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + // Pool name. Does not have to be unique. + Name string `json:"name"` + // The unique ID for the Pool. + ID string `json:"id"` + // A list of load balancer objects IDs. + Loadbalancers []LoadBalancerID `json:"loadbalancers"` + // Indicates whether connections in the same session will be processed by the + // same Pool member or not. + Persistence SessionPersistence `json:"session_persistence"` + // The provider + Provider string `json:"provider"` + Monitor monitors.Monitor `json:"healthmonitor"` +} + +// PoolPage is the page returned by a pager when traversing over a +// collection of pools. +type PoolPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of pools has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r PoolPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"pools_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a PoolPage struct is empty. +func (r PoolPage) IsEmpty() (bool, error) { + is, err := ExtractPools(r) + return len(is) == 0, err +} + +// ExtractPools accepts a Page struct, specifically a PoolPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPools(r pagination.Page) ([]Pool, error) { + var s struct { + Pools []Pool `json:"pools"` + } + err := (r.(PoolPage)).ExtractInto(&s) + return s.Pools, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*Pool, error) { + var s struct { + Pool *Pool `json:"pool"` + } + err := r.ExtractInto(&s) + return s.Pool, err +} + +// CreateResult represents the result of a Create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Member represents the application running on a backend server. +type Member struct { + // Name of the Member. + Name string `json:"name"` + // Weight of Member. + Weight int `json:"weight"` + // The administrative state of the member, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + // Owner of the Member. Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id"` + // parameter value for the subnet UUID. + SubnetID string `json:"subnet_id"` + // The Pool to which the Member belongs. + PoolID string `json:"pool_id"` + // The IP address of the Member. + Address string `json:"address"` + // The port on which the application is hosted. + ProtocolPort int `json:"protocol_port"` + // The unique ID for the Member. + ID string `json:"id"` +} + +// MemberPage is the page returned by a pager when traversing over a +// collection of Members in a Pool. +type MemberPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of members has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r MemberPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"members_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a MemberPage struct is empty. +func (r MemberPage) IsEmpty() (bool, error) { + is, err := ExtractMembers(r) + return len(is) == 0, err +} + +// ExtractMembers accepts a Page struct, specifically a RouterPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMembers(r pagination.Page) ([]Member, error) { + var s struct { + Members []Member `json:"members"` + } + err := (r.(MemberPage)).ExtractInto(&s) + return s.Members, err +} + +type commonMemberResult struct { + gophercloud.Result +} + +// ExtractMember is a function that accepts a result and extracts a router. +func (r commonMemberResult) Extract() (*Member, error) { + var s struct { + Member *Member `json:"member"` + } + err := r.ExtractInto(&s) + return s.Member, err +} + +// CreateMemberResult represents the result of a CreateMember operation. +type CreateMemberResult struct { + commonMemberResult +} + +// GetMemberResult represents the result of a GetMember operation. +type GetMemberResult struct { + commonMemberResult +} + +// UpdateMemberResult represents the result of an UpdateMember operation. +type UpdateMemberResult struct { + commonMemberResult +} + +// DeleteMemberResult represents the result of a DeleteMember operation. +type DeleteMemberResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/urls.go new file mode 100644 index 0000000..bceca67 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/urls.go @@ -0,0 +1,25 @@ +package pools + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "pools" + memberPath = "members" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func memberRootURL(c *gophercloud.ServiceClient, poolId string) string { + return c.ServiceURL(rootPath, resourcePath, poolId, memberPath) +} + +func memberResourceURL(c *gophercloud.ServiceClient, poolID string, memeberID string) string { + return c.ServiceURL(rootPath, resourcePath, poolID, memberPath, memeberID) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index a861210..2fd3d78 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -239,12 +239,42 @@ "revision": "1217c40cf502cef2fc3aa1a25fcd1ed1bdf00a4b", "revisionTime": "2017-07-18T07:00:34Z" }, + { + "checksumSHA1": "CHmnyRSFPivC+b/ojgfeEIY5ReM=", + "path": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips", + "revision": "1217c40cf502cef2fc3aa1a25fcd1ed1bdf00a4b", + "revisionTime": "2017-07-18T07:00:34Z" + }, { "checksumSHA1": "Mjt7GwFygyqPxygY8xZZnUasHmk=", "path": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers", "revision": "1217c40cf502cef2fc3aa1a25fcd1ed1bdf00a4b", "revisionTime": "2017-07-18T07:00:34Z" }, + { + "checksumSHA1": "mhpwj5tPv7Uw5aUfC55fhLPBcKo=", + "path": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners", + "revision": "1217c40cf502cef2fc3aa1a25fcd1ed1bdf00a4b", + "revisionTime": "2017-07-18T07:00:34Z" + }, + { + "checksumSHA1": "5efJz6UH7JCFeav5ZCCzicXCFTU=", + "path": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers", + "revision": "1217c40cf502cef2fc3aa1a25fcd1ed1bdf00a4b", + "revisionTime": "2017-07-18T07:00:34Z" + }, + { + "checksumSHA1": "TVFgBTz7B6bb1R4TWdgAkbE1/fk=", + "path": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors", + "revision": "1217c40cf502cef2fc3aa1a25fcd1ed1bdf00a4b", + "revisionTime": "2017-07-18T07:00:34Z" + }, + { + "checksumSHA1": "xirjw9vJIN6rmkT3T56bfPfOLUM=", + "path": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools", + "revision": "1217c40cf502cef2fc3aa1a25fcd1ed1bdf00a4b", + "revisionTime": "2017-07-18T07:00:34Z" + }, { "checksumSHA1": "S+9nmGWO/5zr/leEGq4qLzU0gsQ=", "path": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding",