diff --git a/Gopkg.lock b/Gopkg.lock index 10a2e4a7e6..3e1a7b5cf3 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -260,7 +260,7 @@ [[projects]] branch = "master" - digest = "1:1569d27c7f3b5dc47a4a834f8dc1a27100928c24d16f3742aac4b89c427e8839" + digest = "1:329a5e71e73ab0ec18a7b90f72db800af2bf2198d2b848d03e2f94279291a106" name = "github.com/gophercloud/gophercloud" packages = [ ".", @@ -280,7 +280,13 @@ "openstack/imageservice/v2/images", "openstack/networking/v2/extensions", "openstack/networking/v2/extensions/attributestags", + "openstack/networking/v2/extensions/layer3/floatingips", "openstack/networking/v2/extensions/layer3/routers", + "openstack/networking/v2/extensions/lbaas_v2/l7policies", + "openstack/networking/v2/extensions/lbaas_v2/listeners", + "openstack/networking/v2/extensions/lbaas_v2/loadbalancers", + "openstack/networking/v2/extensions/lbaas_v2/monitors", + "openstack/networking/v2/extensions/lbaas_v2/pools", "openstack/networking/v2/extensions/security/groups", "openstack/networking/v2/extensions/security/rules", "openstack/networking/v2/extensions/trunks", @@ -486,6 +492,14 @@ revision = "438578804ca6f31be148c27683afc419ce47c06e" version = "v1.3.0" +[[projects]] + digest = "1:077e62d72c79deb8c6ee2eb51173cf66aa8aeea4fa2d6ff3e6d95c71debda612" + name = "github.com/sbueringer/cluster-api-provider-openstack" + packages = ["pkg/cloud/openstack"] + pruneopts = "NUT" + revision = "1df2bf594a48975437509a8eb9c46511e75d70c8" + version = "v0.0.1" + [[projects]] digest = "1:6792bb72ea0e7112157d02e4e175cd421b43d004a853f56316a19beca6e0c074" name = "github.com/spf13/afero" @@ -1165,7 +1179,12 @@ "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images", "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions", "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/attributestags", + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips", "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers", + "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/openstack/networking/v2/extensions/security/groups", "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules", "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/trunks", @@ -1175,15 +1194,19 @@ "github.com/gophercloud/gophercloud/pagination", "github.com/gophercloud/utils/openstack/clientconfig", "github.com/pkg/errors", + "github.com/sbueringer/cluster-api-provider-openstack/pkg/cloud/openstack", "gopkg.in/yaml.v2", "k8s.io/api/core/v1", "k8s.io/apimachinery/pkg/apis/meta/v1", "k8s.io/apimachinery/pkg/runtime", "k8s.io/apimachinery/pkg/runtime/schema", "k8s.io/apimachinery/pkg/runtime/serializer/json", + "k8s.io/apimachinery/pkg/types", "k8s.io/apimachinery/pkg/util/json", "k8s.io/apimachinery/pkg/util/wait", "k8s.io/client-go/kubernetes", + "k8s.io/client-go/tools/clientcmd", + "k8s.io/client-go/tools/clientcmd/api", "k8s.io/client-go/tools/record", "k8s.io/cluster-bootstrap/token/api", "k8s.io/cluster-bootstrap/token/util", @@ -1193,13 +1216,15 @@ "sigs.k8s.io/cluster-api/pkg/apis", "sigs.k8s.io/cluster-api/pkg/apis/cluster/common", "sigs.k8s.io/cluster-api/pkg/apis/cluster/v1alpha1", + "sigs.k8s.io/cluster-api/pkg/client/clientset_generated/clientset", + "sigs.k8s.io/cluster-api/pkg/client/clientset_generated/clientset/typed/cluster/v1alpha1", "sigs.k8s.io/cluster-api/pkg/controller/cluster", "sigs.k8s.io/cluster-api/pkg/controller/machine", "sigs.k8s.io/cluster-api/pkg/errors", "sigs.k8s.io/cluster-api/pkg/util", "sigs.k8s.io/controller-runtime/pkg/client", - "sigs.k8s.io/controller-runtime/pkg/client/config", "sigs.k8s.io/controller-runtime/pkg/manager", + "sigs.k8s.io/controller-runtime/pkg/patch", "sigs.k8s.io/controller-runtime/pkg/runtime/scheme", "sigs.k8s.io/controller-runtime/pkg/runtime/signals", "sigs.k8s.io/controller-tools/cmd/controller-gen", diff --git a/README.md b/README.md index 10da6334b4..59af42c88e 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,9 @@ This provider's versions are compatible with the following versions of Cluster A This provider's versions are able to install and manage the following versions of Kubernetes: -||Kubernetes 1.13.5+|Kubernetes 1.14| -|-|-|-| -|OpenStack Provider v1alpha1 (ea309e7f)|✓|✓| +||Kubernetes 1.13.5+|Kubernetes 1.14|Kubernetes 1.15| +|-|-|-|-| +|OpenStack Provider v1alpha1 (ea309e7f)|✓|✓|✓| Kubernetes control plane and Kubelet versions are defined in `spec.versions.controlPlane` and `spec.versions.kubelet` of `cmd/clusterctl/examples/openstack/machines.yaml.template` respectively. You can generate `cmd/clusterctl/examples/openstack/out/machines.yaml` by running the `generate-yaml.sh` from the template and change the versions if you want. diff --git a/cmd/clusterctl/examples/openstack/machine-deployment.yaml.template b/cmd/clusterctl/examples/openstack/machine-deployment.yaml.template index 2afe37ee77..599d09c2db 100644 --- a/cmd/clusterctl/examples/openstack/machine-deployment.yaml.template +++ b/cmd/clusterctl/examples/openstack/machine-deployment.yaml.template @@ -22,7 +22,6 @@ spec: kind: "OpenstackProviderSpec" flavor: m1.medium image: - sshUserName: keyName: cluster-api-provider-openstack availabilityZone: nova networks: @@ -38,4 +37,4 @@ spec: serverMetadata: key: value versions: - kubelet: 1.14.0 + kubelet: 1.15.0 diff --git a/cmd/clusterctl/examples/openstack/machines.yaml.template b/cmd/clusterctl/examples/openstack/machines.yaml.template index 531b90ca86..2fd4560bab 100644 --- a/cmd/clusterctl/examples/openstack/machines.yaml.template +++ b/cmd/clusterctl/examples/openstack/machines.yaml.template @@ -15,7 +15,6 @@ items: kind: "OpenstackProviderSpec" flavor: m1.medium image: - sshUserName: keyName: cluster-api-provider-openstack availabilityZone: nova networks: @@ -36,8 +35,8 @@ items: serverMetadata: key: value versions: - kubelet: 1.14.0 - controlPlane: 1.14.0 + kubelet: 1.15.0 + controlPlane: 1.15.0 - apiVersion: "cluster.k8s.io/v1alpha1" kind: Machine metadata: @@ -52,7 +51,6 @@ items: kind: "OpenstackProviderSpec" flavor: m1.medium image: - sshUserName: keyName: cluster-api-provider-openstack availabilityZone: nova networks: @@ -69,4 +67,4 @@ items: serverMetadata: key: value versions: - kubelet: 1.14.0 + kubelet: 1.15.0 diff --git a/cmd/clusterctl/examples/openstack/provider-component/user-data/centos/templates/master-user-data.sh b/cmd/clusterctl/examples/openstack/provider-component/user-data/centos/templates/master-user-data.sh index 9bbc01830f..8b0431cb2d 100644 --- a/cmd/clusterctl/examples/openstack/provider-component/user-data/centos/templates/master-user-data.sh +++ b/cmd/clusterctl/examples/openstack/provider-component/user-data/centos/templates/master-user-data.sh @@ -87,6 +87,41 @@ echo $OPENSTACK_CLOUD_PROVIDER_CONF | base64 -d > /etc/kubernetes/cloud.conf mkdir /etc/certs echo $OPENSTACK_CLOUD_CACERT_CONFIG | base64 -d > /etc/certs/cacert + +# Setup certificates +mkdir - /etc/kubernetes/pki /etc/kubernetes/pki/etcd +cat > /etc/kubernetes/pki/ca.crt < /etc/kubernetes/pki/ca.key < /etc/kubernetes/pki/etcd/ca.crt < /etc/kubernetes/pki/etcd/ca.key < /etc/kubernetes/pki/front-proxy-ca.crt < /etc/kubernetes/pki/front-proxy-ca.key < /etc/kubernetes/pki/sa.pub < /etc/kubernetes/pki/sa.key < /etc/kubernetes/kubeadm_config.yaml < /etc/kubernetes/pki/ca.crt < /etc/kubernetes/pki/ca.key < /etc/kubernetes/pki/etcd/ca.crt < /etc/kubernetes/pki/etcd/ca.key < /etc/kubernetes/pki/front-proxy-ca.crt < /etc/kubernetes/pki/front-proxy-ca.key < /etc/kubernetes/pki/sa.pub < /etc/kubernetes/pki/sa.key < @@ -27,3 +28,15 @@ This guide (based on minikube and others should be similar) explains general inf ``` # kubectl --kubeconfig minikube.kubeconfig log clusterapi-controllers-xxxxxxxxx-xxxxx -n openstack-provider-system ``` + +## Master failed to start with error: node xxxx not found + +Sometimes the master machine is created but failed to startup, take ubuntu as example, open `/var/log/messages` +and if you see something like +``` +Jul 10 00:07:58 openstack-master-5wgrw kubelet: E0710 00:07:58.444950 4340 kubelet.go:2248] node "openstack-master-5wgrw" not found +Jul 10 00:07:58 openstack-master-5wgrw kubelet: I0710 00:07:58.526091 4340 kubelet_node_status.go:72] Attempting to register node openstack-master-5wgrw +Jul 10 00:07:58 openstack-master-5wgrw kubelet: E0710 00:07:58.527398 4340 kubelet_node_status.go:94] Unable to register node "openstack-master-5wgrw" with API server: nodes "openstack-master-5wgrw" is forbidden: node "openstack-master-5wgrw.novalocal" is not allowed to modify node "openstack-master-5wgrw" +``` + +This might be caused by [This issue](https://github.com/kubernetes-sigs/cluster-api-provider-openstack/issues/391), try the method proposed there. diff --git a/pkg/apis/openstackproviderconfig/v1alpha1/types.go b/pkg/apis/openstackproviderconfig/v1alpha1/types.go index dd92c51e4c..7e79dfdb1f 100644 --- a/pkg/apis/openstackproviderconfig/v1alpha1/types.go +++ b/pkg/apis/openstackproviderconfig/v1alpha1/types.go @@ -48,9 +48,6 @@ type OpenstackProviderSpec struct { // The ssh key to inject in the instance KeyName string `json:"keyName,omitempty"` - // The machine ssh username - SshUserName string `json:"sshUserName,omitempty"` - // A networks object. Required parameter when there are multiple networks defined for the tenant. // When you do not specify the networks parameter, the server attaches to the only network created for the current tenant. Networks []NetworkParam `json:"networks,omitempty"` @@ -197,12 +194,31 @@ type OpenstackClusterProviderSpec struct { // network, a subnet with NodeCIDR, and a router connected to this subnet. // If you leave this empty, no network will be created. NodeCIDR string `json:"nodeCidr,omitempty"` + // DNSNameservers is the list of nameservers for OpenStack Subnet being created. DNSNameservers []string `json:"dnsNameservers,omitempty"` + // ExternalNetworkID is the ID of an external OpenStack Network. This is necessary // to get public internet to the VMs. ExternalNetworkID string `json:"externalNetworkId,omitempty"` + // ManagedAPIServerLoadBalancer defines whether a LoadBalancer for the + // APIServer should be created. If set to true the following properties are + // mandatory: APIServerLoadBalancerFloatingIP, APIServerLoadBalancerPort + ManagedAPIServerLoadBalancer bool `json:"managedAPIServerLoadBalancer"` + + // APIServerLoadBalancerFloatingIP is the floatingIP which will be associated + // to the APIServer loadbalancer. The floatingIP will be created if it not + // already exists. + APIServerLoadBalancerFloatingIP string `json:"apiServerLoadBalancerFloatingIP,omitempty"` + + // APIServerLoadBalancerPort is the port on which the listener on the APIServer + // loadbalancer will be created + APIServerLoadBalancerPort int `json:"apiServerLoadBalancerPort,omitempty"` + + // APIServerLoadBalancerAdditionalPorts adds additional ports to the APIServerLoadBalancer + APIServerLoadBalancerAdditionalPorts []int `json:"apiServerLoadBalancerAdditionalPorts,omitempty"` + // ManagedSecurityGroups defines that kubernetes manages the OpenStack security groups // for now, that means that we'll create two security groups, one allowing SSH // and API access from everywhere, and another one that allows all traffic to/from @@ -214,6 +230,30 @@ type OpenstackClusterProviderSpec struct { // Default: True. In case of server tag errors, set to False DisableServerTags bool `json:"disableServerTags,omitempty"` + + // CAKeyPair is the key pair for ca certs. + CAKeyPair KeyPair `json:"caKeyPair,omitempty"` + + //EtcdCAKeyPair is the key pair for etcd. + EtcdCAKeyPair KeyPair `json:"etcdCAKeyPair,omitempty"` + + // FrontProxyCAKeyPair is the key pair for FrontProxyKeyPair. + FrontProxyCAKeyPair KeyPair `json:"frontProxyCAKeyPair,omitempty"` + + // SAKeyPair is the service account key pair. + SAKeyPair KeyPair `json:"saKeyPair,omitempty"` +} + +// KeyPair is how operators can supply custom keypairs for kubeadm to use. +type KeyPair struct { + // base64 encoded cert and key + Cert []byte `json:"cert,omitempty"` + Key []byte `json:"key,omitempty"` +} + +// HasCertAndKey returns whether a keypair contains cert and key of non-zero length. +func (kp *KeyPair) HasCertAndKey() bool { + return len(kp.Cert) != 0 && len(kp.Key) != 0 } // +genclient @@ -247,6 +287,10 @@ type Network struct { Subnet *Subnet `json:"subnet,omitempty"` Router *Router `json:"router,omitempty"` + + // Be careful when using this the LoadBalancer is optional and therefore not + // set in all cases + APIServerLoadBalancer *LoadBalancer `json:"apiServerLoadBalancer,omitempty"` } // Subnet represents basic information about the associated OpenStack Neutron Subnet @@ -263,6 +307,14 @@ type Router struct { ID string `json:"id"` } +// LoadBalancer represents basic information about the associated OpenStack LoadBalancer +type LoadBalancer struct { + Name string `json:"name"` + ID string `json:"id"` + IP string `json:"ip"` + InternalIP string `json:"internalIP"` +} + func init() { SchemeBuilder.Register(&OpenstackProviderSpec{}) SchemeBuilder.Register(&OpenstackClusterProviderSpec{}) diff --git a/pkg/apis/openstackproviderconfig/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/openstackproviderconfig/v1alpha1/zz_generated.deepcopy.go index 32f024099c..5fd63f9e96 100644 --- a/pkg/apis/openstackproviderconfig/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/openstackproviderconfig/v1alpha1/zz_generated.deepcopy.go @@ -51,6 +51,48 @@ func (in *Filter) DeepCopy() *Filter { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyPair) DeepCopyInto(out *KeyPair) { + *out = *in + if in.Cert != nil { + in, out := &in.Cert, &out.Cert + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.Key != nil { + in, out := &in.Key, &out.Key + *out = make([]byte, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyPair. +func (in *KeyPair) DeepCopy() *KeyPair { + if in == nil { + return nil + } + out := new(KeyPair) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LoadBalancer) DeepCopyInto(out *LoadBalancer) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancer. +func (in *LoadBalancer) DeepCopy() *LoadBalancer { + if in == nil { + return nil + } + out := new(LoadBalancer) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Network) DeepCopyInto(out *Network) { *out = *in @@ -64,6 +106,11 @@ func (in *Network) DeepCopyInto(out *Network) { *out = new(Router) **out = **in } + if in.APIServerLoadBalancer != nil { + in, out := &in.APIServerLoadBalancer, &out.APIServerLoadBalancer + *out = new(LoadBalancer) + **out = **in + } return } @@ -116,11 +163,20 @@ func (in *OpenstackClusterProviderSpec) DeepCopyInto(out *OpenstackClusterProvid *out = make([]string, len(*in)) copy(*out, *in) } + if in.APIServerLoadBalancerAdditionalPorts != nil { + in, out := &in.APIServerLoadBalancerAdditionalPorts, &out.APIServerLoadBalancerAdditionalPorts + *out = make([]int, len(*in)) + copy(*out, *in) + } if in.Tags != nil { in, out := &in.Tags, &out.Tags *out = make([]string, len(*in)) copy(*out, *in) } + in.CAKeyPair.DeepCopyInto(&out.CAKeyPair) + in.EtcdCAKeyPair.DeepCopyInto(&out.EtcdCAKeyPair) + in.FrontProxyCAKeyPair.DeepCopyInto(&out.FrontProxyCAKeyPair) + in.SAKeyPair.DeepCopyInto(&out.SAKeyPair) return } diff --git a/pkg/cloud/openstack/cluster/actuator.go b/pkg/cloud/openstack/cluster/actuator.go index e75773e372..bbfb8160c0 100644 --- a/pkg/cloud/openstack/cluster/actuator.go +++ b/pkg/cloud/openstack/cluster/actuator.go @@ -11,6 +11,7 @@ import ( "k8s.io/klog" "reflect" providerv1 "sigs.k8s.io/cluster-api-provider-openstack/pkg/apis/openstackproviderconfig/v1alpha1" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/openstack/services/certificates" "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/openstack/services/networking" "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/openstack/services/provider" "sigs.k8s.io/cluster-api-provider-openstack/pkg/deployer" @@ -66,12 +67,26 @@ func (a *Actuator) Reconcile(cluster *clusterv1.Cluster) error { return err } + certificatesService := certificates.NewService() + // Load provider spec & status. clusterProviderSpec, clusterProviderStatus, err := providerv1.ClusterSpecAndStatusFromProviderSpec(cluster) if err != nil { return err } + defer func() { + if err := a.storeCluster(cluster, clusterCopy, clusterProviderSpec, clusterProviderStatus); err != nil { + klog.Errorf("Failed to store cluster %q in namespace %q: %v", cluster.Name, cluster.Namespace, err) + } + }() + + klog.Infof("Reconciling certificates for cluster %s", clusterName) + // Store cert material in spec. + if err := certificatesService.ReconcileCertificates(clusterName, clusterProviderSpec); err != nil { + return errors.Wrapf(err, "failed to reconcile certificates for cluster %q", cluster.Name) + } + klog.Infof("Reconciling network components for cluster %s", clusterName) if clusterProviderSpec.NodeCIDR == "" { klog.V(4).Infof("No need to reconcile network for cluster %s", clusterName) @@ -88,6 +103,12 @@ func (a *Actuator) Reconcile(cluster *clusterv1.Cluster) error { if err != nil { return errors.Errorf("failed to reconcile router: %v", err) } + if clusterProviderSpec.ManagedAPIServerLoadBalancer { + err = networkingService.ReconcileLoadBalancer(clusterName, clusterProviderSpec, clusterProviderStatus) + if err != nil { + return errors.Errorf("failed to reconcile load balancer: %v", err) + } + } } err = networkingService.ReconcileSecurityGroups(clusterName, *clusterProviderSpec, clusterProviderStatus) @@ -95,11 +116,7 @@ func (a *Actuator) Reconcile(cluster *clusterv1.Cluster) error { return errors.Errorf("failed to reconcile security groups: %v", err) } - defer func() { - if err := a.storeCluster(cluster, clusterCopy, clusterProviderSpec, clusterProviderStatus); err != nil { - klog.Errorf("failed to store cluster %q in namespace %q: %v", cluster.Name, cluster.Namespace, err) - } - }() + klog.Infof("Reconciled Cluster %s/%s successfully", cluster.Namespace, cluster.Name) return nil } @@ -142,9 +159,9 @@ func (a *Actuator) Delete(cluster *clusterv1.Cluster) error { return nil } -func (a *Actuator) storeCluster(cluster *clusterv1.Cluster, clusterCopy *clusterv1.Cluster, spec *providerv1.OpenstackClusterProviderSpec, status *providerv1.OpenstackClusterProviderStatus) error { +func (a *Actuator) storeCluster(cluster *clusterv1.Cluster, clusterCopy *clusterv1.Cluster, clusterProviderSpec *providerv1.OpenstackClusterProviderSpec, clusterProviderStatus *providerv1.OpenstackClusterProviderStatus) error { - rawSpec, rawStatus, err := providerv1.EncodeClusterSpecAndStatus(cluster, spec, status) + rawSpec, rawStatus, err := providerv1.EncodeClusterSpecAndStatus(cluster, clusterProviderSpec, clusterProviderStatus) if err != nil { return err } @@ -174,6 +191,25 @@ func (a *Actuator) storeCluster(cluster *clusterv1.Cluster, clusterCopy *cluster cluster.ResourceVersion = result.ResourceVersion } + // set to APIServerLoadBalancer IP & Port for now. With generated kubeadm + // this can be changed to clusterConfiguration.controlPlaneEndpoint for the + // non-LoadBalancer case. For now it doesn't matter because the Status is not + // used yet. + apiServerHost := clusterProviderSpec.APIServerLoadBalancerFloatingIP + apiServerPort := clusterProviderSpec.APIServerLoadBalancerPort + if clusterProviderSpec.ManagedAPIServerLoadBalancer { + if apiServerHost != clusterProviderStatus.Network.APIServerLoadBalancer.IP { + return fmt.Errorf("APIServerLoadBalancer has IP %s instead of %s", clusterProviderStatus.Network.APIServerLoadBalancer.IP, apiServerHost) + } + } + // Check if API endpoints is not set or has changed. + if cluster.Status.APIEndpoints == nil || cluster.Status.APIEndpoints[0].Host != apiServerHost { + cluster.Status.APIEndpoints = append(cluster.Status.APIEndpoints, clusterv1.APIEndpoint{ + Host: apiServerHost, + Port: apiServerPort, + }) + } + cluster.Status.ProviderStatus = rawStatus if !reflect.DeepEqual(cluster.Status, clusterCopy.Status) { klog.Infof("Updating cluster status %s", cluster.Name) diff --git a/pkg/cloud/openstack/machine/actuator.go b/pkg/cloud/openstack/machine/actuator.go index 68e35282e7..6259f90157 100644 --- a/pkg/cloud/openstack/machine/actuator.go +++ b/pkg/cloud/openstack/machine/actuator.go @@ -26,6 +26,7 @@ import ( "reflect" constants "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/openstack/contants" "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/openstack/services/compute" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/openstack/services/networking" "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/openstack/services/provider" "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/openstack/services/userdata" "sigs.k8s.io/cluster-api-provider-openstack/pkg/deployer" @@ -95,7 +96,12 @@ func (a *Actuator) Create(ctx context.Context, cluster *clusterv1.Cluster, machi return err } - clusterProviderSpec, err := providerv1.ClusterSpecFromProviderSpec(cluster.Spec.ProviderSpec) + networkingService, err := networking.NewService(osProviderClient, clientOpts) + if err != nil { + return err + } + + clusterProviderSpec, clusterProviderStatus, err := providerv1.ClusterSpecAndStatusFromProviderSpec(cluster) if err != nil { return a.handleMachineError(machine, apierrors.CreateMachine( "error creating Openstack instance: %v", err)) @@ -116,35 +122,34 @@ func (a *Actuator) Create(ctx context.Context, cluster *clusterv1.Cluster, machi } if instance != nil { klog.Infof("Skipped creating a VM that already exists.\n") - return nil - } - - userData, err := userdata.GetUserData(a.params.Client, a.params.KubeClient, machineProviderSpec, cluster, machine) - if err != nil { - if machineError, ok := err.(*apierrors.MachineError); ok { - return a.handleMachineError(machine, machineError) + } else { + userData, err := userdata.GetUserData(a.params.Client, a.params.KubeClient, machineProviderSpec, cluster, machine) + if err != nil { + if machineError, ok := err.(*apierrors.MachineError); ok { + return a.handleMachineError(machine, machineError) + } + return err } - return err - } - instance, err = computeService.InstanceCreate(clusterName, machine.Name, clusterProviderSpec, machineProviderSpec, userData, machineProviderSpec.KeyName) + instance, err = computeService.InstanceCreate(clusterName, machine.Name, clusterProviderSpec, machineProviderSpec, userData, machineProviderSpec.KeyName) - if err != nil { - return a.handleMachineError(machine, apierrors.CreateMachine( - "error creating Openstack instance: %v", err)) - } - instanceCreateTimeout := getTimeout("CLUSTER_API_OPENSTACK_INSTANCE_CREATE_TIMEOUT", TimeoutInstanceCreate) - instanceCreateTimeout = instanceCreateTimeout * time.Minute - err = util.PollImmediate(RetryIntervalInstanceStatus, instanceCreateTimeout, func() (bool, error) { - instance, err := computeService.GetInstance(instance.ID) if err != nil { - return false, nil + return a.handleMachineError(machine, apierrors.CreateMachine( + "error creating Openstack instance: %v", err)) + } + instanceCreateTimeout := getTimeout("CLUSTER_API_OPENSTACK_INSTANCE_CREATE_TIMEOUT", TimeoutInstanceCreate) + instanceCreateTimeout = instanceCreateTimeout * time.Minute + err = util.PollImmediate(RetryIntervalInstanceStatus, instanceCreateTimeout, func() (bool, error) { + instance, err := computeService.GetInstance(instance.ID) + if err != nil { + return false, nil + } + return instance.Status == "ACTIVE", nil + }) + if err != nil { + return a.handleMachineError(machine, apierrors.CreateMachine( + "error creating Openstack instance: %v", err)) } - return instance.Status == "ACTIVE", nil - }) - if err != nil { - return a.handleMachineError(machine, apierrors.CreateMachine( - "error creating Openstack instance: %v", err)) } if machineProviderSpec.FloatingIP != "" { @@ -153,11 +158,24 @@ func (a *Actuator) Create(ctx context.Context, cluster *clusterv1.Cluster, machi return a.handleMachineError(machine, apierrors.CreateMachine( "Associate floatingIP err: %v", err)) } + } + if clusterProviderSpec.ManagedAPIServerLoadBalancer { + err := networkingService.ReconcileLoadBalancerMember(clusterName, machine, clusterProviderStatus) + if err != nil { + return a.handleMachineError(machine, apierrors.CreateMachine( + "Reconcile LoadBalancer Member err: %v", err)) + } } record.Eventf(machine, "CreatedInstance", "Created new instance with id: %s", instance.ID) - return a.updateAnnotation(machine, instance.ID) + err = a.updateAnnotation(machine, instance.ID) + if err != nil { + return err + } + + klog.Infof("Created Machine %s/%s: %s successfully", cluster.Namespace, cluster.Name, machine.Name) + return nil } func (a *Actuator) Delete(ctx context.Context, cluster *clusterv1.Cluster, machine *clusterv1.Machine) error { @@ -167,6 +185,8 @@ func (a *Actuator) Delete(ctx context.Context, cluster *clusterv1.Cluster, machi } klog.Infof("Deleting Machine %s/%s: %s", cluster.Namespace, cluster.Name, machine.Name) + clusterName := fmt.Sprintf("%s-%s", cluster.ObjectMeta.Namespace, cluster.Name) + osProviderClient, clientOpts, err := provider.NewClientFromMachine(a.params.KubeClient, machine) if err != nil { return err @@ -177,6 +197,22 @@ func (a *Actuator) Delete(ctx context.Context, cluster *clusterv1.Cluster, machi return err } + networkingService, err := networking.NewService(osProviderClient, clientOpts) + if err != nil { + return err + } + + clusterProviderStatus, err := providerv1.ClusterStatusFromProviderStatus(cluster.Status.ProviderStatus) + if err != nil { + return a.handleMachineError(machine, apierrors.CreateMachine( + "error updating Openstack instance: %v", err)) + } + + err = networkingService.DeleteLoadBbalancerMember(clusterName, machine, clusterProviderStatus) + if err != nil { + return err + } + instance, err := a.instanceExists(machine) if err != nil { return err @@ -204,11 +240,30 @@ func (a *Actuator) Update(ctx context.Context, cluster *clusterv1.Cluster, machi } klog.Infof("Updating Machine %s/%s: %s", cluster.Namespace, cluster.Name, machine.Name) + clusterName := fmt.Sprintf("%s-%s", cluster.ObjectMeta.Namespace, cluster.Name) + + osProviderClient, clientOpts, err := provider.NewClientFromMachine(a.params.KubeClient, machine) + if err != nil { + return err + } + + networkingService, err := networking.NewService(osProviderClient, clientOpts) + if err != nil { + return err + } + + clusterProviderStatus, err := providerv1.ClusterStatusFromProviderStatus(cluster.Status.ProviderStatus) + if err != nil { + return a.handleMachineError(machine, apierrors.CreateMachine( + "error updating Openstack instance: %v", err)) + } + status, err := a.instanceStatus(machine) if err != nil { return err } + var machineAlreadyExists bool currentMachine := (*clusterv1.Machine)(status) if currentMachine == nil { instance, err := a.instanceExists(machine) @@ -217,47 +272,58 @@ func (a *Actuator) Update(ctx context.Context, cluster *clusterv1.Cluster, machi } if instance != nil && instance.Status == "ACTIVE" { klog.Infof("Populating current state for boostrap machine %v", machine.ObjectMeta.Name) - return a.updateAnnotation(machine, instance.ID) + err = a.updateAnnotation(machine, instance.ID) + if err != nil { + return err + } + machineAlreadyExists = true } else { return fmt.Errorf("cannot retrieve current state to update machine %v", machine.ObjectMeta.Name) } } if !requiresUpdate(currentMachine, machine) { - return nil + machineAlreadyExists = true } - if util.IsControlPlaneMachine(currentMachine) { - // TODO: add master inplace - klog.Errorf("master inplace update failed: not support master in place update now") - } else { - klog.Infof("re-creating machine %s for update.", currentMachine.ObjectMeta.Name) - err = a.Delete(ctx, cluster, currentMachine) - if err != nil { - klog.Errorf("delete machine %s for update failed: %v", currentMachine.ObjectMeta.Name, err) + if !machineAlreadyExists { + if util.IsControlPlaneMachine(currentMachine) { + // TODO: add master inplace + klog.Errorf("master inplace update failed: not support master in place update now") } else { - instanceDeleteTimeout := getTimeout("CLUSTER_API_OPENSTACK_INSTANCE_DELETE_TIMEOUT", TimeoutInstanceDelete) - instanceDeleteTimeout = instanceDeleteTimeout * time.Minute - err = util.PollImmediate(RetryIntervalInstanceStatus, instanceDeleteTimeout, func() (bool, error) { - instance, err := a.instanceExists(machine) + klog.Infof("re-creating machine %s for update.", currentMachine.ObjectMeta.Name) + err = a.Delete(ctx, cluster, currentMachine) + if err != nil { + klog.Errorf("delete machine %s for update failed: %v", currentMachine.ObjectMeta.Name, err) + } else { + instanceDeleteTimeout := getTimeout("CLUSTER_API_OPENSTACK_INSTANCE_DELETE_TIMEOUT", TimeoutInstanceDelete) + instanceDeleteTimeout = instanceDeleteTimeout * time.Minute + err = util.PollImmediate(RetryIntervalInstanceStatus, instanceDeleteTimeout, func() (bool, error) { + instance, err := a.instanceExists(machine) + if err != nil { + return false, nil + } + return instance == nil, nil + }) if err != nil { - return false, nil + return a.handleMachineError(machine, apierrors.DeleteMachine( + "error deleting Openstack instance: %v", err)) } - return instance == nil, nil - }) - if err != nil { - return a.handleMachineError(machine, apierrors.DeleteMachine( - "error deleting Openstack instance: %v", err)) - } - err = a.Create(ctx, cluster, machine) - if err != nil { - klog.Errorf("create machine %s for update failed: %v", machine.ObjectMeta.Name, err) + err = a.Create(ctx, cluster, machine) + if err != nil { + klog.Errorf("create machine %s for update failed: %v", machine.ObjectMeta.Name, err) + } + klog.Infof("Successfully updated machine %s", currentMachine.ObjectMeta.Name) } - klog.Infof("Successfully updated machine %s", currentMachine.ObjectMeta.Name) } } + err = networkingService.ReconcileLoadBalancerMember(clusterName, machine, clusterProviderStatus) + if err != nil { + return err + } + return nil } diff --git a/pkg/cloud/openstack/options/options.go b/pkg/cloud/openstack/options/options.go index f15d4bc6e5..6f57b4f2bb 100644 --- a/pkg/cloud/openstack/options/options.go +++ b/pkg/cloud/openstack/options/options.go @@ -22,9 +22,13 @@ import ( ) var ( - TokenTTL time.Duration + TokenTTL time.Duration + UserDataFolder string + UserDataPostprocessor string ) func init() { flag.DurationVar(&TokenTTL, "token_ttl", 60*time.Minute, "TTL for kubeadm bootstrap token of the target Kubernetes cluster") + flag.StringVar(&UserDataFolder, "user-data-folder", "", "if set, user data files are retrieved from /.yaml") + flag.StringVar(&UserDataPostprocessor, "user-data-postprocessor", "", "postprocessor to user for the user data") } diff --git a/pkg/cloud/openstack/services/certificates/BUILD.bazel b/pkg/cloud/openstack/services/certificates/BUILD.bazel new file mode 100644 index 0000000000..6dc36a93a2 --- /dev/null +++ b/pkg/cloud/openstack/services/certificates/BUILD.bazel @@ -0,0 +1,27 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "certificates.go", + "service.go", + ], + importpath = "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/aws/services/certificates", + visibility = ["//visibility:public"], + deps = [ + "//pkg/apis/awsprovider/v1alpha1:go_default_library", + "//pkg/cloud/aws/actuators:go_default_library", + "//vendor/github.com/pkg/errors:go_default_library", + "//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["certificates_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/apis/awsprovider/v1alpha1:go_default_library", + "//vendor/github.com/pkg/errors:go_default_library", + ], +) diff --git a/pkg/cloud/openstack/services/certificates/certificates.go b/pkg/cloud/openstack/services/certificates/certificates.go new file mode 100644 index 0000000000..9f9e3ae8ed --- /dev/null +++ b/pkg/cloud/openstack/services/certificates/certificates.go @@ -0,0 +1,334 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package certificates + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "encoding/pem" + "fmt" + "github.com/pkg/errors" + "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/klog" + "math" + "math/big" + "net" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/apis/openstackproviderconfig/v1alpha1" + "strings" + "time" +) + +const ( + rsaKeySize = 2048 + duration365d = time.Hour * 24 * 365 + clusterCA = "cluster-ca" + etcdCA = "etcd-ca" + frontProxyCA = "front-proxy-ca" + serviceAccount = "service-account" +) + +// NewPrivateKey creates an RSA private key +func NewPrivateKey() (*rsa.PrivateKey, error) { + return rsa.GenerateKey(rand.Reader, rsaKeySize) +} + +// AltNames contains the domain names and IP addresses that will be added +// to the API Server's x509 certificate SubAltNames field. The values will +// be passed directly to the x509.Certificate object. +type AltNames struct { + DNSNames []string + IPs []net.IP +} + +// Config contains the basic fields required for creating a certificate +type Config struct { + CommonName string + Organization []string + AltNames AltNames + Usages []x509.ExtKeyUsage +} + +// ReconcileCertificates generate certificates if none exists. +func (s *Service) ReconcileCertificates(clusterName string, clusterProviderSpec *v1alpha1.OpenstackClusterProviderSpec) error { + if !clusterProviderSpec.CAKeyPair.HasCertAndKey() { + klog.Infof("Generating keypair for user %s", clusterCA) + clusterCAKeyPair, err := generateCACert(&clusterProviderSpec.CAKeyPair, clusterCA) + if err != nil { + return errors.Wrapf(err, "Failed to generate certs for %q", clusterCA) + } + clusterProviderSpec.CAKeyPair = clusterCAKeyPair + } + + if !clusterProviderSpec.EtcdCAKeyPair.HasCertAndKey() { + klog.Infof("Generating keypair for user %s", etcdCA) + etcdCAKeyPair, err := generateCACert(&clusterProviderSpec.EtcdCAKeyPair, etcdCA) + if err != nil { + return errors.Wrapf(err, "Failed to generate certs for %q", etcdCA) + } + clusterProviderSpec.EtcdCAKeyPair = etcdCAKeyPair + } + if !clusterProviderSpec.FrontProxyCAKeyPair.HasCertAndKey() { + klog.Infof("Generating keypair for user %s", frontProxyCA) + fpCAKeyPair, err := generateCACert(&clusterProviderSpec.FrontProxyCAKeyPair, frontProxyCA) + if err != nil { + return errors.Wrapf(err, "Failed to generate certs for %q", frontProxyCA) + } + clusterProviderSpec.FrontProxyCAKeyPair = fpCAKeyPair + } + + if !clusterProviderSpec.SAKeyPair.HasCertAndKey() { + klog.Infof("Generating service account keys for user %s", serviceAccount) + saKeyPair, err := generateServiceAccountKeys(&clusterProviderSpec.SAKeyPair, serviceAccount) + if err != nil { + return errors.Wrapf(err, "Failed to generate keyPair for %q", serviceAccount) + } + clusterProviderSpec.SAKeyPair = saKeyPair + } + return nil +} + +func generateCACert(kp *v1alpha1.KeyPair, user string) (v1alpha1.KeyPair, error) { + x509Cert, privKey, err := NewCertificateAuthority() + if err != nil { + return v1alpha1.KeyPair{}, errors.Wrapf(err, "failed to generate CA cert for %q", user) + } + if kp == nil { + return v1alpha1.KeyPair{ + Cert: EncodeCertPEM(x509Cert), + Key: EncodePrivateKeyPEM(privKey), + }, nil + } + kp.Cert = EncodeCertPEM(x509Cert) + kp.Key = EncodePrivateKeyPEM(privKey) + return *kp, nil +} + +func generateServiceAccountKeys(kp *v1alpha1.KeyPair, user string) (v1alpha1.KeyPair, error) { + saCreds, err := NewPrivateKey() + if err != nil { + return v1alpha1.KeyPair{}, errors.Wrapf(err, "failed to create service account public and private keys") + } + saPub, err := EncodePublicKeyPEM(&saCreds.PublicKey) + if err != nil { + return v1alpha1.KeyPair{}, errors.Wrapf(err, "failed to encode service account public key to PEM") + } + if kp == nil { + return v1alpha1.KeyPair{ + Cert: saPub, + Key: EncodePrivateKeyPEM(saCreds), + }, nil + } + kp.Cert = saPub + kp.Key = EncodePrivateKeyPEM(saCreds) + return *kp, nil +} + +// NewSignedCert creates a signed certificate using the given CA certificate and key +func (cfg *Config) NewSignedCert(key *rsa.PrivateKey, caCert *x509.Certificate, caKey *rsa.PrivateKey) (*x509.Certificate, error) { + serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64)) + if err != nil { + return nil, errors.Wrap(err, "failed to generate random integer for signed cerficate") + } + + if len(cfg.CommonName) == 0 { + return nil, errors.New("must specify a CommonName") + } + + if len(cfg.Usages) == 0 { + return nil, errors.New("must specify at least one ExtKeyUsage") + } + + tmpl := x509.Certificate{ + Subject: pkix.Name{ + CommonName: cfg.CommonName, + Organization: cfg.Organization, + }, + DNSNames: cfg.AltNames.DNSNames, + IPAddresses: cfg.AltNames.IPs, + SerialNumber: serial, + NotBefore: caCert.NotBefore, + NotAfter: time.Now().Add(duration365d).UTC(), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: cfg.Usages, + } + + b, err := x509.CreateCertificate(rand.Reader, &tmpl, caCert, key.Public(), caKey) + if err != nil { + return nil, errors.Wrapf(err, "failed to create signed certificate: %+v", tmpl) + } + + return x509.ParseCertificate(b) +} + +// NewCertificateAuthority creates new certificate and private key for the certificate authority +func NewCertificateAuthority() (*x509.Certificate, *rsa.PrivateKey, error) { + key, err := NewPrivateKey() + if err != nil { + return nil, nil, errors.Wrap(err, "unable to create private key") + } + + cert, err := NewSelfSignedCACert(key) + if err != nil { + return nil, nil, errors.Wrap(err, "unable to create self-signed certificate") + } + + return cert, key, nil +} + +// NewSelfSignedCACert creates a CA certificate. +func NewSelfSignedCACert(key *rsa.PrivateKey) (*x509.Certificate, error) { + cfg := Config{ + CommonName: "kubernetes", + } + + now := time.Now().UTC() + + tmpl := x509.Certificate{ + SerialNumber: new(big.Int).SetInt64(0), + Subject: pkix.Name{ + CommonName: cfg.CommonName, + Organization: cfg.Organization, + }, + NotBefore: now, + NotAfter: now.Add(duration365d * 10), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + MaxPathLenZero: true, + BasicConstraintsValid: true, + MaxPathLen: 0, + IsCA: true, + } + + b, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, key.Public(), key) + if err != nil { + return nil, errors.Wrapf(err, "failed to create self signed CA certificate: %+v", tmpl) + } + + return x509.ParseCertificate(b) +} + +// NewKubeconfig creates a new Kubeconfig where endpoint is the ELB endpoint. +func NewKubeconfig(clusterName, endpoint string, caCert *x509.Certificate, caKey *rsa.PrivateKey) (*api.Config, error) { + cfg := &Config{ + CommonName: "kubernetes-admin", + Organization: []string{"system:masters"}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + + clientKey, err := NewPrivateKey() + if err != nil { + return nil, errors.Wrap(err, "unable to create private key") + } + + clientCert, err := cfg.NewSignedCert(clientKey, caCert, caKey) + if err != nil { + return nil, errors.Wrap(err, "unable to sign certificate") + } + + userName := "kubernetes-admin" + contextName := fmt.Sprintf("%s@%s", userName, clusterName) + + return &api.Config{ + Clusters: map[string]*api.Cluster{ + clusterName: { + Server: endpoint, + CertificateAuthorityData: EncodeCertPEM(caCert), + }, + }, + Contexts: map[string]*api.Context{ + contextName: { + Cluster: clusterName, + AuthInfo: userName, + }, + }, + AuthInfos: map[string]*api.AuthInfo{ + userName: { + ClientKeyData: EncodePrivateKeyPEM(clientKey), + ClientCertificateData: EncodeCertPEM(clientCert), + }, + }, + CurrentContext: contextName, + }, nil +} + +// EncodeCertPEM returns PEM-endcoded certificate data. +func EncodeCertPEM(cert *x509.Certificate) []byte { + block := pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + } + return pem.EncodeToMemory(&block) +} + +// EncodePrivateKeyPEM returns PEM-encoded private key data. +func EncodePrivateKeyPEM(key *rsa.PrivateKey) []byte { + block := pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + } + + return pem.EncodeToMemory(&block) +} + +// EncodePublicKeyPEM returns PEM-encoded public key data. +func EncodePublicKeyPEM(key *rsa.PublicKey) ([]byte, error) { + der, err := x509.MarshalPKIXPublicKey(key) + if err != nil { + return []byte{}, err + } + block := pem.Block{ + Type: "PUBLIC KEY", + Bytes: der, + } + return pem.EncodeToMemory(&block), nil +} + +// DecodeCertPEM attempts to return a decoded certificate or nil +// if the encoded input does not contain a certificate. +func DecodeCertPEM(encoded []byte) (*x509.Certificate, error) { + block, _ := pem.Decode(encoded) + if block == nil { + return nil, nil + } + + return x509.ParseCertificate(block.Bytes) +} + +// DecodePrivateKeyPEM attempts to return a decoded key or nil +// if the encoded input does not contain a private key. +func DecodePrivateKeyPEM(encoded []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(encoded) + if block == nil { + return nil, nil + } + + return x509.ParsePKCS1PrivateKey(block.Bytes) +} + +// GenerateCertificateHash returns the encoded sha256 hash for the certificate provided +func GenerateCertificateHash(encoded []byte) (string, error) { + cert, err := DecodeCertPEM(encoded) + if err != nil || cert == nil { + return "", errors.Errorf("failed to parse PEM block containing the public key") + } + + certHash := sha256.Sum256(cert.RawSubjectPublicKeyInfo) + return "sha256:" + strings.ToLower(hex.EncodeToString(certHash[:])), nil +} diff --git a/pkg/cloud/openstack/services/certificates/certificates_test.go b/pkg/cloud/openstack/services/certificates/certificates_test.go new file mode 100644 index 0000000000..0f687a9755 --- /dev/null +++ b/pkg/cloud/openstack/services/certificates/certificates_test.go @@ -0,0 +1,223 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package certificates + +import ( + "crypto/x509" + "testing" + + "github.com/pkg/errors" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/apis/openstackproviderconfig/v1alpha1" +) + +func TestGenerateCACert(t *testing.T) { + testCases := []struct { + name string + inputKeyPair *v1alpha1.KeyPair + inputUser string + expectKeyPairGen bool + expectedError error + }{ + { + name: "should generate keypair when inputKeyPair==nil", + inputKeyPair: nil, + inputUser: "foo-ca", + expectKeyPairGen: true, + expectedError: nil, + }, + { + name: "should generate keypair when inputKeyPair has no cert", + inputKeyPair: &v1alpha1.KeyPair{Key: []byte("foo-key")}, + inputUser: "foo-ca", + expectKeyPairGen: true, + expectedError: nil, + }, + { + name: "should generate keypair when inputKeyPair has no key", + inputKeyPair: &v1alpha1.KeyPair{Cert: []byte("foo-cert")}, + inputUser: "foo-ca", + expectKeyPairGen: true, + expectedError: nil, + }, + { + name: "should generate keypair when inputKeyPair has no cert and nokey", + inputKeyPair: &v1alpha1.KeyPair{}, + inputUser: "foo-ca", + expectKeyPairGen: true, + expectedError: nil, + }, + { + name: "should not generate keypair when inputKeyPair has cert and key", + inputKeyPair: &v1alpha1.KeyPair{Cert: []byte("foo-cert"), Key: []byte("foo-key")}, + inputUser: "foo-ca", + expectKeyPairGen: false, + expectedError: nil, + }, + } + for _, tc := range testCases { + actualKeyPair, actualError := generateCACert(tc.inputKeyPair, tc.inputUser) + if tc.expectedError != nil { + if tc.expectedError.Error() != actualError.Error() { + t.Fatalf("[%s], Unexpected error, Want [%v], Got: [%v]", tc.name, tc.expectedError, actualError) + } + continue + } + if !tc.expectKeyPairGen { + if len(tc.inputKeyPair.Cert) != len(actualKeyPair.Cert) || string(tc.inputKeyPair.Cert) != string(actualKeyPair.Cert) { + t.Fatalf("[%s] Want cert=%q, Got cert=%q", tc.name, string(tc.inputKeyPair.Cert), string(actualKeyPair.Cert)) + } + if len(tc.inputKeyPair.Key) != len(actualKeyPair.Key) || string(tc.inputKeyPair.Key) != string(actualKeyPair.Key) { + t.Fatalf("[%s] Want key=%q, Got key=%q", tc.name, string(tc.inputKeyPair.Key), string(actualKeyPair.Key)) + } + } else { + _, decodeErr := DecodeCertPEM(actualKeyPair.Cert) + if decodeErr != nil { + t.Fatalf("[%s], Expected to decode generated cert, Got decode failure %v", tc.name, decodeErr) + } + _, decodeErr = DecodePrivateKeyPEM(actualKeyPair.Key) + if decodeErr != nil { + t.Fatalf("[%s], Expected to decode generated private key, Got decode failure failed %v", tc.name, decodeErr) + } + } + } +} + +func TestGenerateServiceAccountKeys(t *testing.T) { + testCases := []struct { + name string + inputKeyPair *v1alpha1.KeyPair + inputUser string + expectKeyPairGen bool + expectedError error + }{ + { + name: "should generate keypair when inputKeyPair==nil", + inputKeyPair: nil, + inputUser: "foo-sa", + expectKeyPairGen: true, + expectedError: nil, + }, + { + name: "should generate keypair when inputKeyPair has no cert", + inputKeyPair: &v1alpha1.KeyPair{Key: []byte("foo-key")}, + inputUser: "foo-sa", + expectKeyPairGen: true, + expectedError: nil, + }, + { + name: "should generate keypair when inputKeyPair has no key", + inputKeyPair: &v1alpha1.KeyPair{Cert: []byte("foo-cert")}, + inputUser: "foo-sa", + expectKeyPairGen: true, + expectedError: nil, + }, + { + name: "should generate keypair when inputKeyPair has no cert and nokey", + inputKeyPair: &v1alpha1.KeyPair{}, + inputUser: "foo-ca", + expectKeyPairGen: true, + expectedError: nil, + }, + { + name: "should not generate keypair when inputKeyPair has cert and key", + inputKeyPair: &v1alpha1.KeyPair{Cert: []byte("foo-cert"), Key: []byte("foo-key")}, + inputUser: "foo-sa", + expectKeyPairGen: false, + expectedError: nil, + }, + } + for _, tc := range testCases { + actualKeyPair, actualError := generateServiceAccountKeys(tc.inputKeyPair, tc.inputUser) + if tc.expectedError != nil { + if tc.expectedError.Error() != actualError.Error() { + t.Fatalf("[%s], Unexpected error, Want [%v], Got: [%v]", tc.name, tc.expectedError, actualError) + } + continue + } + if !tc.expectKeyPairGen { + if len(tc.inputKeyPair.Cert) != len(actualKeyPair.Cert) || string(tc.inputKeyPair.Cert) != string(actualKeyPair.Cert) { + t.Fatalf("[%s] Want cert=%q, Got cert=%q", tc.name, string(tc.inputKeyPair.Cert), string(actualKeyPair.Cert)) + } + if len(tc.inputKeyPair.Key) != len(actualKeyPair.Key) || string(tc.inputKeyPair.Key) != string(actualKeyPair.Key) { + t.Fatalf("[%s] Want key=%q, Got key=%q", tc.name, string(tc.inputKeyPair.Key), string(actualKeyPair.Key)) + } + } else { + _, decodeErr := DecodePrivateKeyPEM(actualKeyPair.Key) + if decodeErr != nil { + t.Fatalf("[%s], Expected to decode generated private key, Got decode failure failed %v", tc.name, decodeErr) + } + + // TODO: find a stronger check + if len(actualKeyPair.Key) <= 0 { + t.Fatalf("[%s], Expected to public key of length > 0, Got public key of length %d", tc.name, len(actualKeyPair.Key)) + } + } + } +} + +func TestNewCertificateAuthority(t *testing.T) { + testCases := []struct { + name string + testFunc func() error + }{ + { + name: "should generate conformant ca certificate", + testFunc: func() error { + cert, _, err := NewCertificateAuthority() + if err != nil { + return err + } + + if !cert.MaxPathLenZero { + return errors.Errorf("Unexpected value for MaxPathLenZero, Want: [true]; Got: [%t]", cert.MaxPathLenZero) + } + + if cert.MaxPathLen != 0 { + return errors.Errorf("Unexpected value for MaxPathLen, Want: [0]; Got: [%d]", cert.MaxPathLen) + } + + if !cert.BasicConstraintsValid { + return errors.Errorf("Unexpected value for BasicConstraintsValid, Want: [true]; Got: [%t]", cert.BasicConstraintsValid) + } + + expectedUsage := x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign + actualUsage := cert.KeyUsage + if expectedUsage != actualUsage { + return errors.Errorf("Unexpected value for KeyUsage, Want: [%d]; Got: [%d]", expectedUsage, actualUsage) + } + + expectedCommonName := "kubernetes" + actualCommonName := cert.Subject.CommonName + if expectedCommonName != actualCommonName { + return errors.Errorf("Unexpected CommonName, Want: [%s]; Got: [%s]", expectedCommonName, actualCommonName) + } + + if !cert.IsCA { + return errors.Errorf("Unexpected value for IsCA, Want: [true]; Got: [%t]", cert.IsCA) + } + + return nil + }, + }, + } + + for _, tc := range testCases { + if err := tc.testFunc(); err != nil { + t.Fatalf("[%s] failed: %v", tc.name, err) + } + } +} diff --git a/pkg/cloud/openstack/services/certificates/service.go b/pkg/cloud/openstack/services/certificates/service.go new file mode 100644 index 0000000000..c61cc7e83f --- /dev/null +++ b/pkg/cloud/openstack/services/certificates/service.go @@ -0,0 +1,27 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package certificates + +// Service groups certificate related operations together and allows +// certificate updates to be applied to the actuator scope. +type Service struct { +} + +// NewService returns a new certificates service for the given actuators scope. +func NewService() *Service { + return &Service{} +} diff --git a/pkg/cloud/openstack/services/networking/loadbalancer.go b/pkg/cloud/openstack/services/networking/loadbalancer.go new file mode 100644 index 0000000000..8316b687db --- /dev/null +++ b/pkg/cloud/openstack/services/networking/loadbalancer.go @@ -0,0 +1,427 @@ +package networking + +import ( + "fmt" + "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" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/klog" + providerv1 "sigs.k8s.io/cluster-api-provider-openstack/pkg/apis/openstackproviderconfig/v1alpha1" + constants "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/openstack/contants" + clusterv1 "sigs.k8s.io/cluster-api/pkg/apis/cluster/v1alpha1" + "sigs.k8s.io/cluster-api/pkg/util" + "time" +) + +func (s *Service) ReconcileLoadBalancer(clusterName string, clusterProviderSpec *providerv1.OpenstackClusterProviderSpec, clusterProviderStatus *providerv1.OpenstackClusterProviderStatus) error { + + if clusterProviderSpec.ExternalNetworkID == "" { + klog.V(3).Info("No need to create loadbalancer, due to missing ExternalNetworkID") + return nil + } + if clusterProviderSpec.APIServerLoadBalancerFloatingIP == "" { + klog.V(3).Info("No need to create loadbalancer, due to missing APIServerLoadBalancerFloatingIP") + return nil + } + if clusterProviderSpec.APIServerLoadBalancerPort == 0 { + klog.V(3).Info("No need to create loadbalancer, due to missing APIServerLoadBalancerPort") + return nil + } + + loadBalancerName := fmt.Sprintf("%s-cluster-%s-%s", networkPrefix, clusterName, kubeapiLBSuffix) + klog.Infof("Reconciling loadbalancer %s", loadBalancerName) + + // lb + lb, err := checkIfLbExists(s.client, loadBalancerName) + if err != nil { + return err + } + if lb == nil { + klog.Infof("Creating loadbalancer %s", loadBalancerName) + lbCreateOpts := loadbalancers.CreateOpts{ + Name: loadBalancerName, + VipSubnetID: clusterProviderStatus.Network.Subnet.ID, + } + + lb, err = loadbalancers.Create(s.client, lbCreateOpts).Extract() + if err != nil { + return fmt.Errorf("error creating loadbalancer: %s", err) + } + err = waitForLoadBalancer(s.client, lb.ID, "ACTIVE") + if err != nil { + return err + } + } + + // floating ip + fp, err := checkIfFloatingIPExists(s.client, clusterProviderSpec.APIServerLoadBalancerFloatingIP) + if err != nil { + return err + } + if fp == nil { + klog.Infof("Creating floating ip %s", clusterProviderSpec.APIServerLoadBalancerFloatingIP) + fpCreateOpts := &floatingips.CreateOpts{ + FloatingIP: clusterProviderSpec.APIServerLoadBalancerFloatingIP, + FloatingNetworkID: clusterProviderSpec.ExternalNetworkID, + PortID: lb.VipPortID, + } + fp, err = floatingips.Create(s.client, fpCreateOpts).Extract() + if err != nil { + return fmt.Errorf("error allocating floating IP: %s", err) + } + err = waitForFloatingIP(s.client, fp.ID, "ACTIVE") + if err != nil { + return err + } + } + + // lb listener + portList := []int{clusterProviderSpec.APIServerLoadBalancerPort} + portList = append(portList, clusterProviderSpec.APIServerLoadBalancerAdditionalPorts...) + for _, port := range portList { + lbPortObjectsName := fmt.Sprintf("%s-%d", loadBalancerName, port) + + listener, err := checkIfListenerExists(s.client, lbPortObjectsName) + if err != nil { + return err + } + if listener == nil { + klog.Infof("Creating lb listener %s", lbPortObjectsName) + listenerCreateOpts := listeners.CreateOpts{ + Name: lbPortObjectsName, + Protocol: "TCP", + ProtocolPort: port, + LoadbalancerID: lb.ID, + //DefaultPoolID: d.Get("default_pool_id").(string), TODO check vio4/5 // remove before PR + } + listener, err = listeners.Create(s.client, listenerCreateOpts).Extract() + if err != nil { + return fmt.Errorf("error creating listener: %s", err) + } + err = waitForLoadBalancer(s.client, lb.ID, "ACTIVE") + if err != nil { + return err + } + err = waitForListener(s.client, listener.ID, "ACTIVE") + if err != nil { + return err + } + } + + // lb pool + pool, err := checkIfPoolExists(s.client, lbPortObjectsName) + if err != nil { + return err + } + if pool == nil { + klog.Infof("Creating lb pool %s", lbPortObjectsName) + poolCreateOpts := pools.CreateOpts{ + Name: lbPortObjectsName, + Protocol: "TCP", + LBMethod: pools.LBMethodRoundRobin, + ListenerID: listener.ID, + } + pool, err = pools.Create(s.client, poolCreateOpts).Extract() + if err != nil { + return fmt.Errorf("error creating pool: %s", err) + } + err = waitForLoadBalancer(s.client, lb.ID, "ACTIVE") + if err != nil { + return err + } + } + + // lb monitor + monitor, err := checkIfMonitorExists(s.client, lbPortObjectsName) + if err != nil { + return err + } + if monitor == nil { + klog.Infof("Creating lb monitor %s", lbPortObjectsName) + monitorCreateOpts := monitors.CreateOpts{ + Name: lbPortObjectsName, + PoolID: pool.ID, + Type: "TCP", + Delay: 30, + Timeout: 5, + MaxRetries: 3, + } + _, err = monitors.Create(s.client, monitorCreateOpts).Extract() + if err != nil { + return fmt.Errorf("error creating monitor: %s", err) + } + err = waitForLoadBalancer(s.client, lb.ID, "ACTIVE") + if err != nil { + return err + } + } + } + + clusterProviderStatus.Network.APIServerLoadBalancer = &providerv1.LoadBalancer{ + Name: lb.Name, + ID: lb.ID, + InternalIP: lb.VipAddress, + IP: fp.FloatingIP, + } + return nil +} + +func (s *Service) ReconcileLoadBalancerMember(clusterName string, machine *clusterv1.Machine, clusterProviderStatus *providerv1.OpenstackClusterProviderStatus) error { + + if !util.IsControlPlaneMachine(machine) { + return nil + } + + loadBalancerName := fmt.Sprintf("%s-cluster-%s-%s", networkPrefix, clusterName, kubeapiLBSuffix) + + lbID := clusterProviderStatus.Network.APIServerLoadBalancer.ID + subnetID := clusterProviderStatus.Network.Subnet.ID + + for _, port := range []int{22, 6443} { + lbPortObjectsName := fmt.Sprintf("%s-%d", loadBalancerName, port) + name := lbPortObjectsName + "-" + machine.Name + + pool, err := checkIfPoolExists(s.client, lbPortObjectsName) + if err != nil { + return err + } + + ip, ok := machine.ObjectMeta.Annotations[constants.OpenstackIPAnnotationKey] + if !ok { + klog.Infof("no ip found yet on annotation %s on machine %s", constants.OpenstackIPAnnotationKey, machine.Name) + return nil + } + + lbMember, err := checkIfLbMemberExists(s.client, pool.ID, name) + if err != nil { + return err + } + + if lbMember != nil { + // check if we have to recreate the LB Member + if lbMember.Address == ip { + // nothing to do return + return nil + } + + klog.Infof("Deleting lb member %s (because the IP of the machine changed)", name) + + // lb member changed so let's delete it so we can create it again with the correct IP + err = waitForLoadBalancer(s.client, lbID, "ACTIVE") + if err != nil { + return err + } + err = pools.DeleteMember(s.client, pool.ID, lbMember.ID).ExtractErr() + if err != nil { + return fmt.Errorf("error deleting lbmember: %s", err) + } + err = waitForLoadBalancer(s.client, lbID, "ACTIVE") + if err != nil { + return err + } + } + + klog.Infof("Creating lb member %s", name) + + // if we got to this point we should either create or re-create the lb member + lbMemberOpts := pools.CreateMemberOpts{ + Name: name, + ProtocolPort: port, + Address: ip, + SubnetID: subnetID, + } + + err = waitForLoadBalancer(s.client, lbID, "ACTIVE") + if err != nil { + return err + } + lbMember, err = pools.CreateMember(s.client, pool.ID, lbMemberOpts).Extract() + if err != nil { + return fmt.Errorf("error create lbmember: %s", err) + } + err = waitForLoadBalancer(s.client, lbID, "ACTIVE") + if err != nil { + return err + } + } + return nil +} + +func (s *Service) DeleteLoadBbalancerMember(clusterName string, machine *clusterv1.Machine, clusterProviderStatus *providerv1.OpenstackClusterProviderStatus) error { + + if machine == nil || !util.IsControlPlaneMachine(machine) { + return nil + } + + loadBalancerName := fmt.Sprintf("%s-cluster-%s-%s", networkPrefix, clusterName, kubeapiLBSuffix) + + lbID := clusterProviderStatus.Network.APIServerLoadBalancer.ID + + for _, port := range []int{22, 6443} { + lbPortObjectsName := fmt.Sprintf("%s-%d", loadBalancerName, port) + name := lbPortObjectsName + "-" + machine.Name + + pool, err := checkIfPoolExists(s.client, lbPortObjectsName) + if err != nil { + return err + } + + lbMember, err := checkIfLbMemberExists(s.client, pool.ID, name) + if err != nil { + return err + } + + if lbMember != nil { + + // lb member changed so let's delete it so we can create it again with the correct IP + err = waitForLoadBalancer(s.client, lbID, "ACTIVE") + if err != nil { + return err + } + err = pools.DeleteMember(s.client, pool.ID, lbMember.ID).ExtractErr() + if err != nil { + return fmt.Errorf("error deleting lbmember: %s", err) + } + err = waitForLoadBalancer(s.client, lbID, "ACTIVE") + if err != nil { + return err + } + } + } + return nil +} + +func checkIfLbExists(networkingClient *gophercloud.ServiceClient, name string) (*loadbalancers.LoadBalancer, error) { + allPages, err := loadbalancers.List(networkingClient, loadbalancers.ListOpts{Name: name}).AllPages() + if err != nil { + return nil, err + } + lbList, err := loadbalancers.ExtractLoadBalancers(allPages) + if err != nil { + return nil, err + } + if len(lbList) == 0 { + return nil, nil + } + return &lbList[0], nil +} + +func checkIfFloatingIPExists(networkingClient *gophercloud.ServiceClient, ip string) (*floatingips.FloatingIP, error) { + allPages, err := floatingips.List(networkingClient, floatingips.ListOpts{FloatingIP: ip}).AllPages() + if err != nil { + return nil, err + } + fpList, err := floatingips.ExtractFloatingIPs(allPages) + if err != nil { + return nil, err + } + if len(fpList) == 0 { + return nil, nil + } + return &fpList[0], nil +} + +func checkIfListenerExists(networkingClient *gophercloud.ServiceClient, name string) (*listeners.Listener, error) { + allPages, err := listeners.List(networkingClient, listeners.ListOpts{Name: name}).AllPages() + if err != nil { + return nil, err + } + listenerList, err := listeners.ExtractListeners(allPages) + if err != nil { + return nil, err + } + if len(listenerList) == 0 { + return nil, nil + } + return &listenerList[0], nil +} + +func checkIfPoolExists(networkingClient *gophercloud.ServiceClient, name string) (*pools.Pool, error) { + allPages, err := pools.List(networkingClient, pools.ListOpts{Name: name}).AllPages() + if err != nil { + return nil, err + } + poolList, err := pools.ExtractPools(allPages) + if err != nil { + return nil, err + } + if len(poolList) == 0 { + return nil, nil + } + return &poolList[0], nil +} + +func checkIfMonitorExists(networkingClient *gophercloud.ServiceClient, name string) (*monitors.Monitor, error) { + allPages, err := monitors.List(networkingClient, monitors.ListOpts{Name: name}).AllPages() + if err != nil { + return nil, err + } + monitorList, err := monitors.ExtractMonitors(allPages) + if err != nil { + return nil, err + } + if len(monitorList) == 0 { + return nil, nil + } + return &monitorList[0], nil +} + +func checkIfLbMemberExists(networkingClient *gophercloud.ServiceClient, poolID, name string) (*pools.Member, error) { + allPages, err := pools.ListMembers(networkingClient, poolID, pools.ListMembersOpts{Name: name}).AllPages() + if err != nil { + return nil, err + } + lbMemberList, err := pools.ExtractMembers(allPages) + if err != nil { + return nil, err + } + if len(lbMemberList) == 0 { + return nil, nil + } + return &lbMemberList[0], nil +} + +var backoff = wait.Backoff{ + Steps: 10, + Duration: 30 * time.Second, + Factor: 1.0, + Jitter: 0.1, +} + +func waitForLoadBalancer(networkingClient *gophercloud.ServiceClient, id, target string) error { + klog.Infof("Waiting for loadbalancer %s to become %s.", id, target) + return wait.ExponentialBackoff(backoff, func() (bool, error) { + lb, err := loadbalancers.Get(networkingClient, id).Extract() + if err != nil { + return false, err + } + return lb.ProvisioningStatus == target, nil + }) +} + +func waitForFloatingIP(networkingClient *gophercloud.ServiceClient, id, target string) error { + klog.Infof("Waiting for floatingip %s to become %s.", id, target) + return wait.ExponentialBackoff(backoff, func() (bool, error) { + fp, err := floatingips.Get(networkingClient, id).Extract() + if err != nil { + return false, err + } + return fp.Status == target, nil + }) +} + +func waitForListener(networkingClient *gophercloud.ServiceClient, id, target string) error { + klog.Infof("Waiting for listener %s to become %s.", id, target) + return wait.ExponentialBackoff(backoff, func() (bool, error) { + _, err := listeners.Get(networkingClient, id).Extract() + if err != nil { + return false, err + } + // The listener resource has no Status attribute, so a successful Get is the best we can do + return true, nil + }) +} diff --git a/pkg/cloud/openstack/services/networking/service.go b/pkg/cloud/openstack/services/networking/service.go index 9d671f6ede..ba2a2d2d27 100644 --- a/pkg/cloud/openstack/services/networking/service.go +++ b/pkg/cloud/openstack/services/networking/service.go @@ -25,7 +25,8 @@ import ( ) const ( - networkPrefix string = "k8s" + networkPrefix string = "k8s" + kubeapiLBSuffix string = "kubeapi" ) // Service interfaces with the OpenStack Networking API. diff --git a/pkg/cloud/openstack/services/userdata/machinescript.go b/pkg/cloud/openstack/services/userdata/machinescript.go index abf2f5691e..1d4d9b6446 100644 --- a/pkg/cloud/openstack/services/userdata/machinescript.go +++ b/pkg/cloud/openstack/services/userdata/machinescript.go @@ -20,11 +20,18 @@ import ( "bytes" "context" "errors" + "io/ioutil" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" "k8s.io/klog" + "path" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/apis/openstackproviderconfig/v1alpha1" "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/openstack/options" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/deployer" "sigs.k8s.io/cluster-api/pkg/util" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" + "strings" "text/template" "time" @@ -54,6 +61,15 @@ type setupParams struct { Machine *clusterv1.Machine MachineSpec *openstackconfigv1.OpenstackProviderSpec + CACert string + CAKey string + EtcdCACert string + EtcdCAKey string + FrontProxyCACert string + FrontProxyCAKey string + SaCert string + SaKey string + PodCIDR string ServiceCIDR string GetMasterEndpoint func() (string, error) @@ -95,6 +111,15 @@ func GetUserData(controllerClient client.Client, kubeClient kubernetes.Interface p, postprocess = userDataSecret.Data[PostprocessorKey] postprocessor = string(p) + } else if options.UserDataFolder != "" { + userData, err = ioutil.ReadFile(path.Join(options.UserDataFolder, fmt.Sprintf("%s.yaml", machine.Name))) + if err != nil { + return "", fmt.Errorf("could not load local userdata files: %v", err) + } + postprocessor = options.UserDataPostprocessor + if postprocessor != "" { + postprocess = true + } } var userDataRendered string @@ -159,7 +184,12 @@ func createBootstrapToken(controllerClient client.Client) (string, error) { panic(fmt.Sprintf("unable to create token. there might be a bug somwhere: %v", err)) } - err = controllerClient.Create(context.TODO(), tokenSecret) + kubeClient, err := getWorkloadClusterKubeClient(controllerClient) + if err != nil { + return "", err + } + + err = kubeClient.Create(context.TODO(), tokenSecret) if err != nil { return "", err } @@ -170,21 +200,99 @@ func createBootstrapToken(controllerClient client.Client) (string, error) { ), nil } +func getWorkloadClusterKubeClient(controllerClient client.Client) (client.Client, error) { + var master *clusterv1.Machine + var cluster *clusterv1.Cluster + + msList := &clusterv1.MachineList{} + listOptions := &client.ListOptions{} + err := controllerClient.List(context.TODO(), listOptions, msList) + if err != nil { + return nil, fmt.Errorf("error retrieving machines: %v", err) + } + for _, m := range msList.Items { + if util.IsControlPlaneMachine(&m) { + master = &m + break + } + } + if master == nil { + return nil, fmt.Errorf("could not find master node") + } + + clusterList := &clusterv1.ClusterList{} + err = controllerClient.List(context.TODO(), listOptions, clusterList) + if err != nil { + return nil, fmt.Errorf("error retrieving clusters: %v", err) + } + for _, c := range clusterList.Items { + if clusterName, ok := master.Labels[clusterv1.MachineClusterLabelName]; ok && clusterName == c.Name { + cluster = &c + } + } + if cluster == nil { + return nil, fmt.Errorf("could not find cluster") + } + + kubeConfig, err := deployer.New().GetKubeConfig(cluster, master) + if err != nil { + return nil, err + } + + apiConfig, err := clientcmd.Load([]byte(kubeConfig)) + if err != nil { + return nil, err + } + + cfg, err := clientcmd.NewDefaultClientConfig(*apiConfig, &clientcmd.ConfigOverrides{}).ClientConfig() + if err != nil { + return nil, err + } + + mgr, err := manager.New(cfg, manager.Options{}) + if err != nil { + return nil, fmt.Errorf("unable to create manager for restConfig: %v", err) + } + + return mgr.GetClient(), nil +} + func masterStartupScript(cluster *clusterv1.Cluster, machine *clusterv1.Machine, script string) (string, error) { - machineSpec, err := openstackconfigv1.MachineSpecFromProviderSpec(machine.Spec.ProviderSpec) + clusterProviderSpec, err := openstackconfigv1.ClusterSpecFromProviderSpec(cluster.Spec.ProviderSpec) + if err != nil { + return "", err + } + + if err := validateCertificates(clusterProviderSpec); err != nil { + return "", err + } + + machineProviderSpec, err := openstackconfigv1.MachineSpecFromProviderSpec(machine.Spec.ProviderSpec) if err != nil { return "", err } params := setupParams{ - Cluster: cluster, - Machine: machine, - MachineSpec: machineSpec, - PodCIDR: getSubnet(cluster.Spec.ClusterNetwork.Pods), - ServiceCIDR: getSubnet(cluster.Spec.ClusterNetwork.Services), + Cluster: cluster, + CACert: string(clusterProviderSpec.CAKeyPair.Cert), + CAKey: string(clusterProviderSpec.CAKeyPair.Key), + EtcdCACert: string(clusterProviderSpec.EtcdCAKeyPair.Cert), + EtcdCAKey: string(clusterProviderSpec.EtcdCAKeyPair.Key), + FrontProxyCACert: string(clusterProviderSpec.FrontProxyCAKeyPair.Cert), + FrontProxyCAKey: string(clusterProviderSpec.FrontProxyCAKeyPair.Key), + SaCert: string(clusterProviderSpec.SAKeyPair.Cert), + SaKey: string(clusterProviderSpec.SAKeyPair.Key), + Machine: machine, + MachineSpec: machineProviderSpec, + PodCIDR: getSubnet(cluster.Spec.ClusterNetwork.Pods), + ServiceCIDR: getSubnet(cluster.Spec.ClusterNetwork.Services), + } + + fMap := map[string]interface{}{ + "Indent": templateYAMLIndent, } - masterStartUpScript := template.Must(template.New("masterStartUp").Parse(script)) + masterStartUpScript := template.Must(template.New("masterStartUp").Funcs(fMap).Parse(script)) var buf bytes.Buffer if err := masterStartUpScript.Execute(&buf, params); err != nil { @@ -194,7 +302,7 @@ func masterStartupScript(cluster *clusterv1.Cluster, machine *clusterv1.Machine, } func nodeStartupScript(cluster *clusterv1.Cluster, machine *clusterv1.Machine, token, script string) (string, error) { - machineSpec, err := openstackconfigv1.MachineSpecFromProviderSpec(machine.Spec.ProviderSpec) + machineProviderSpec, err := openstackconfigv1.MachineSpecFromProviderSpec(machine.Spec.ProviderSpec) if err != nil { return "", err } @@ -210,7 +318,7 @@ func nodeStartupScript(cluster *clusterv1.Cluster, machine *clusterv1.Machine, t Token: token, Cluster: cluster, Machine: machine, - MachineSpec: machineSpec, + MachineSpec: machineProviderSpec, PodCIDR: getSubnet(cluster.Spec.ClusterNetwork.Pods), ServiceCIDR: getSubnet(cluster.Spec.ClusterNetwork.Services), GetMasterEndpoint: GetMasterEndpoint, @@ -225,6 +333,29 @@ func nodeStartupScript(cluster *clusterv1.Cluster, machine *clusterv1.Machine, t return buf.String(), nil } +func validateCertificates(clusterProviderSpec *v1alpha1.OpenstackClusterProviderSpec) error { + if !isKeyPairValid(clusterProviderSpec.CAKeyPair.Cert, clusterProviderSpec.CAKeyPair.Key) { + return errors.New("CA cert material in the ClusterProviderSpec is missing cert/key") + } + + if !isKeyPairValid(clusterProviderSpec.EtcdCAKeyPair.Cert, clusterProviderSpec.EtcdCAKeyPair.Key) { + return errors.New("ETCD CA cert material in the ClusterProviderSpec is missing cert/key") + } + + if !isKeyPairValid(clusterProviderSpec.FrontProxyCAKeyPair.Cert, clusterProviderSpec.FrontProxyCAKeyPair.Key) { + return errors.New("FrontProxy CA cert material in ClusterProviderSpec is missing cert/key") + } + + if !isKeyPairValid(clusterProviderSpec.SAKeyPair.Cert, clusterProviderSpec.SAKeyPair.Key) { + return errors.New("ServiceAccount cert material in ClusterProviderSpec is missing cert/key") + } + return nil +} + +func isKeyPairValid(cert, key []byte) bool { + return len(cert) > 0 && len(key) > 0 +} + func getEndpoint(apiEndpoint clusterv1.APIEndpoint) string { return fmt.Sprintf("%s:%d", apiEndpoint.Host, apiEndpoint.Port) } @@ -236,3 +367,9 @@ func getSubnet(netRange clusterv1.NetworkRanges) string { } return netRange.CIDRBlocks[0] } + +func templateYAMLIndent(i int, input string) string { + split := strings.Split(input, "\n") + ident := "\n" + strings.Repeat(" ", i) + return strings.Repeat(" ", i) + strings.Join(split, ident) +} diff --git a/pkg/deployer/deployer.go b/pkg/deployer/deployer.go index 590de80d4f..34bfb3f011 100644 --- a/pkg/deployer/deployer.go +++ b/pkg/deployer/deployer.go @@ -3,13 +3,12 @@ package deployer import ( "fmt" "github.com/pkg/errors" + "k8s.io/client-go/tools/clientcmd" "k8s.io/klog" - "os" providerv1 "sigs.k8s.io/cluster-api-provider-openstack/pkg/apis/openstackproviderconfig/v1alpha1" constants "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/openstack/contants" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/openstack/services/certificates" clusterv1 "sigs.k8s.io/cluster-api/pkg/apis/cluster/v1alpha1" - "sigs.k8s.io/cluster-api/pkg/util" - "strings" ) // Deployer satisfies the ProviderDeployer(https://github.com/kubernetes-sigs/cluster-api/blob/master/cmd/clusterctl/clusterdeployer/clusterdeployer.go) interface. @@ -35,31 +34,47 @@ func (d *Deployer) GetIP(cluster *clusterv1.Cluster, machine *clusterv1.Machine) // GetKubeConfig returns the kubeConfig after the bootstrap process is complete. func (d *Deployer) GetKubeConfig(cluster *clusterv1.Cluster, master *clusterv1.Machine) (string, error) { - ip, err := d.GetIP(cluster, master) + + // Load provider config. + clusterProviderSpec, err := providerv1.ClusterSpecFromProviderSpec(cluster.Spec.ProviderSpec) + if err != nil { + return "", errors.Errorf("failed to load cluster provider status: %v", err) + } + + cert, err := certificates.DecodeCertPEM(clusterProviderSpec.CAKeyPair.Cert) + if err != nil { + return "", errors.Wrap(err, "failed to decode CA Cert") + } else if cert == nil { + return "", errors.New("certificate not found in clusterProviderSpec") + } + + key, err := certificates.DecodePrivateKeyPEM(clusterProviderSpec.CAKeyPair.Key) if err != nil { - return "", err + return "", errors.Wrap(err, "failed to decode private key") + } else if key == nil { + return "", errors.New("key not found in clusterProviderSpec") } - homeDir, ok := os.LookupEnv("HOME") - if !ok { - return "", fmt.Errorf("unable to use HOME environment variable to find SSH key: %v", err) + var apiServerEndpoint string + if clusterProviderSpec.ManagedAPIServerLoadBalancer { + apiServerEndpoint = fmt.Sprintf("https://%s:%d", clusterProviderSpec.APIServerLoadBalancerFloatingIP, clusterProviderSpec.APIServerLoadBalancerPort) + } else { + ip, err := d.GetIP(cluster, master) + if err != nil { + return "", err + } + apiServerEndpoint = fmt.Sprintf("https://%s:6443", ip) } - machineSpec, err := providerv1.MachineSpecFromProviderSpec(master.Spec.ProviderSpec) + cfg, err := certificates.NewKubeconfig(cluster.Name, apiServerEndpoint, cert, key) if err != nil { - return "", err + return "", errors.Wrap(err, "failed to generate a kubeconfig") } - result := strings.TrimSpace(util.ExecCommand( - "ssh", "-i", homeDir+"/.ssh/openstack_tmp", - "-o", "StrictHostKeyChecking no", - "-o", "UserKnownHostsFile /dev/null", - "-o", "BatchMode=yes", - fmt.Sprintf("%s@%s", machineSpec.SshUserName, ip), - "echo STARTFILE; sudo cat /etc/kubernetes/admin.conf")) - parts := strings.Split(result, "STARTFILE") - if len(parts) != 2 { - return "", nil + yaml, err := clientcmd.Write(*cfg) + if err != nil { + return "", errors.Wrap(err, "failed to serialize config to yaml") } - return strings.TrimSpace(parts[1]), nil + + return string(yaml), nil } diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/doc.go new file mode 100644 index 0000000000..a71a3ec88a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/doc.go @@ -0,0 +1,71 @@ +/* +package floatingips enables management and retrieval of Floating IPs from the +OpenStack Networking service. + +Example to List Floating IPs + + listOpts := floatingips.ListOpts{ + FloatingNetworkID: "a6917946-38ab-4ffd-a55a-26c0980ce5ee", + } + + allPages, err := floatingips.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allFIPs, err := floatingips.ExtractFloatingIPs(allPages) + if err != nil { + panic(err) + } + + for _, fip := range allFIPs { + fmt.Printf("%+v\n", fip) + } + +Example to Create a Floating IP + + createOpts := floatingips.CreateOpts{ + FloatingNetworkID: "a6917946-38ab-4ffd-a55a-26c0980ce5ee", + } + + fip, err := floatingips.Create(networkingClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Floating IP + + fipID := "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + portID := "76d0a61b-b8e5-490c-9892-4cf674f2bec8" + + updateOpts := floatingips.UpdateOpts{ + PortID: &portID, + } + + fip, err := floatingips.Update(networkingClient, fipID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Disassociate a Floating IP with a Port + + fipID := "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + + updateOpts := floatingips.UpdateOpts{ + PortID: new(string), + } + + fip, err := floatingips.Update(networkingClient, fipID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Floating IP + + fipID := "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + err := floatingips.Delete(networkClient, fipID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package floatingips 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 0000000000..0c0db64d8d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go @@ -0,0 +1,182 @@ +package floatingips + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFloatingIPListQuery() (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 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"` + Description string `q:"description"` + 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"` + ProjectID string `q:"project_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + RouterID string `q:"router_id"` + Status string `q:"status"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` +} + +// ToNetworkListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFloatingIPListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// 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 ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToFloatingIPListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return FloatingIPPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +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 { + Description string `json:"description,omitempty"` + 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"` + SubnetID string `json:"subnet_id,omitempty"` + TenantID string `json:"tenant_id,omitempty"` + ProjectID string `json:"project_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 allows extensions to add additional parameters to the +// Update request. +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 { + Description *string `json:"description,omitempty"` + PortID *string `json:"port_id,omitempty"` + FixedIP string `json:"fixed_ip_address,omitempty"` +} + +// ToFloatingIPUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder +// interface +func (opts UpdateOpts) ToFloatingIPUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "floatingip") + if err != nil { + return nil, err + } + + if m := b["floatingip"].(map[string]interface{}); m["port_id"] == "" { + m["port_id"] = nil + } + + return b, nil +} + +// 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 0000000000..a9709ccec3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go @@ -0,0 +1,131 @@ +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 { + // ID is the unique identifier for the floating IP instance. + ID string `json:"id"` + + // Description for the floating IP instance. + Description string `json:"description"` + + // FloatingNetworkID is the UUID of the external network where the floating + // IP is to be created. + FloatingNetworkID string `json:"floating_network_id"` + + // FloatingIP is the address of the floating IP on the external network. + FloatingIP string `json:"floating_ip_address"` + + // PortID is the UUID of the port on an internal network that is associated + // with the floating IP. + PortID string `json:"port_id"` + + // FixedIP is the specific IP address of the internal port which should be + // associated with the floating IP. + FixedIP string `json:"fixed_ip_address"` + + // TenantID is the project owner of the floating IP. Only admin users can + // specify a project identifier other than its own. + TenantID string `json:"tenant_id"` + + // ProjectID is the project owner of the floating IP. + ProjectID string `json:"project_id"` + + // Status is the condition of the API resource. + Status string `json:"status"` + + // RouterID is the ID of the router used for this floating IP. + RouterID string `json:"router_id"` + + // Tags optionally set via extensions/attributestags + Tags []string `json:"tags"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will extract a FloatingIP resource from a result. +func (r commonResult) Extract() (*FloatingIP, error) { + var s FloatingIP + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "floatingip") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a FloatingIP. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a FloatingIP. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a FloatingIP. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of an update operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +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 FloatingIPPage 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 +} + +func ExtractFloatingIPsInto(r pagination.Page, v interface{}) error { + return r.(FloatingIPPage).Result.ExtractIntoSlicePtr(v, "floatingips") +} 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 0000000000..1318a184ca --- /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/l7policies/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/l7policies/doc.go new file mode 100644 index 0000000000..813579905c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/l7policies/doc.go @@ -0,0 +1,123 @@ +/* +Package l7policies provides information and interaction with L7Policies and +Rules of the LBaaS v2 extension for the OpenStack Networking service. + +Example to Create a L7Policy + + createOpts := l7policies.CreateOpts{ + Name: "redirect-example.com", + ListenerID: "023f2e34-7806-443b-bfae-16c324569a3d", + Action: l7policies.ActionRedirectToURL, + RedirectURL: "http://www.example.com", + } + l7policy, err := l7policies.Create(lbClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to List L7Policies + + listOpts := l7policies.ListOpts{ + ListenerID: "c79a4468-d788-410c-bf79-9a8ef6354852", + } + allPages, err := l7policies.List(lbClient, listOpts).AllPages() + if err != nil { + panic(err) + } + allL7Policies, err := l7policies.ExtractL7Policies(allPages) + if err != nil { + panic(err) + } + for _, l7policy := range allL7Policies { + fmt.Printf("%+v\n", l7policy) + } + +Example to Get a L7Policy + + l7policy, err := l7policies.Get(lbClient, "023f2e34-7806-443b-bfae-16c324569a3d").Extract() + if err != nil { + panic(err) + } + +Example to Delete a L7Policy + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + err := l7policies.Delete(lbClient, l7policyID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Update a L7Policy + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + name := "new-name" + updateOpts := l7policies.UpdateOpts{ + Name: &name, + } + l7policy, err := l7policies.Update(lbClient, l7policyID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Rule + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + createOpts := l7policies.CreateRuleOpts{ + RuleType: l7policies.TypePath, + CompareType: l7policies.CompareTypeRegex, + Value: "/images*", + } + rule, err := l7policies.CreateRule(lbClient, l7policyID, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to List L7 Rules + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + listOpts := l7policies.ListRulesOpts{ + RuleType: l7policies.TypePath, + } + allPages, err := l7policies.ListRules(lbClient, l7policyID, listOpts).AllPages() + if err != nil { + panic(err) + } + allRules, err := l7policies.ExtractRules(allPages) + if err != nil { + panic(err) + } + for _, rule := allRules { + fmt.Printf("%+v\n", rule) + } + +Example to Get a l7 rule + + l7rule, err := l7policies.GetRule(lbClient, "023f2e34-7806-443b-bfae-16c324569a3d", "53ad8ab8-40fa-11e8-a508-00224d6b7bc1").Extract() + if err != nil { + panic(err) + } + +Example to Delete a l7 rule + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + ruleID := "64dba99f-8af8-4200-8882-e32a0660f23e" + err := l7policies.DeleteRule(lbClient, l7policyID, ruleID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Update a Rule + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + ruleID := "64dba99f-8af8-4200-8882-e32a0660f23e" + updateOpts := l7policies.UpdateRuleOpts{ + RuleType: l7policies.TypePath, + CompareType: l7policies.CompareTypeRegex, + Value: "/images/special*", + } + rule, err := l7policies.UpdateRule(lbClient, l7policyID, ruleID, updateOpts).Extract() + if err != nil { + panic(err) + } +*/ +package l7policies diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/l7policies/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/l7policies/requests.go new file mode 100644 index 0000000000..9d2b3a0d35 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/l7policies/requests.go @@ -0,0 +1,376 @@ +package l7policies + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToL7PolicyCreateMap() (map[string]interface{}, error) +} + +type Action string +type RuleType string +type CompareType string + +const ( + ActionRedirectToPool Action = "REDIRECT_TO_POOL" + ActionRedirectToURL Action = "REDIRECT_TO_URL" + ActionReject Action = "REJECT" + + TypeCookie RuleType = "COOKIE" + TypeFileType RuleType = "FILE_TYPE" + TypeHeader RuleType = "HEADER" + TypeHostName RuleType = "HOST_NAME" + TypePath RuleType = "PATH" + + CompareTypeContains CompareType = "CONTAINS" + CompareTypeEndWith CompareType = "ENDS_WITH" + CompareTypeEqual CompareType = "EQUAL_TO" + CompareTypeRegex CompareType = "REGEX" + CompareTypeStartWith CompareType = "STARTS_WITH" +) + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Name of the L7 policy. + Name string `json:"name,omitempty"` + + // The ID of the listener. + ListenerID string `json:"listener_id" required:"true"` + + // The L7 policy action. One of REDIRECT_TO_POOL, REDIRECT_TO_URL, or REJECT. + Action Action `json:"action" required:"true"` + + // The position of this policy on the listener. + Position int32 `json:"position,omitempty"` + + // A human-readable description for the resource. + Description string `json:"description,omitempty"` + + // TenantID is the UUID of the tenant who owns the L7 policy in octavia. + // Only administrative users can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + + // Requests matching this policy will be redirected to the pool with this ID. + // Only valid if action is REDIRECT_TO_POOL. + RedirectPoolID string `json:"redirect_pool_id,omitempty"` + + // Requests matching this policy will be redirected to this URL. + // Only valid if action is REDIRECT_TO_URL. + RedirectURL string `json:"redirect_url,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToL7PolicyCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToL7PolicyCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "l7policy") +} + +// Create accepts a CreateOpts struct and uses the values to create a new l7policy. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToL7PolicyCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToL7PolicyListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. +type ListOpts struct { + Name string `q:"name"` + Description string `q:"description"` + ListenerID string `q:"listener_id"` + Action string `q:"action"` + TenantID string `q:"tenant_id"` + RedirectPoolID string `q:"redirect_pool_id"` + RedirectURL string `q:"redirect_url"` + Position int32 `q:"position"` + AdminStateUp bool `q:"admin_state_up"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToL7PolicyListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToL7PolicyListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// l7policies. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those l7policies that are owned by the +// project 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.ToL7PolicyListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return L7PolicyPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a particular l7policy 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 +} + +// Delete will permanently delete a particular l7policy based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToL7PolicyUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Name of the L7 policy, empty string is allowed. + Name *string `json:"name,omitempty"` + + // The L7 policy action. One of REDIRECT_TO_POOL, REDIRECT_TO_URL, or REJECT. + Action Action `json:"action,omitempty"` + + // The position of this policy on the listener. + Position int32 `json:"position,omitempty"` + + // A human-readable description for the resource, empty string is allowed. + Description *string `json:"description,omitempty"` + + // Requests matching this policy will be redirected to the pool with this ID. + // Only valid if action is REDIRECT_TO_POOL. + RedirectPoolID *string `json:"redirect_pool_id,omitempty"` + + // Requests matching this policy will be redirected to this URL. + // Only valid if action is REDIRECT_TO_URL. + RedirectURL *string `json:"redirect_url,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToL7PolicyUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToL7PolicyUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "l7policy") + if err != nil { + return nil, err + } + + m := b["l7policy"].(map[string]interface{}) + + if m["redirect_pool_id"] == "" { + m["redirect_pool_id"] = nil + } + + if m["redirect_url"] == "" { + m["redirect_url"] = nil + } + + return b, nil +} + +// Update allows l7policy to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToL7PolicyUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// CreateRuleOpts is the common options struct used in this package's CreateRule +// operation. +type CreateRuleOpts struct { + // The L7 rule type. One of COOKIE, FILE_TYPE, HEADER, HOST_NAME, or PATH. + RuleType RuleType `json:"type" required:"true"` + + // The comparison type for the L7 rule. One of CONTAINS, ENDS_WITH, EQUAL_TO, REGEX, or STARTS_WITH. + CompareType CompareType `json:"compare_type" required:"true"` + + // The value to use for the comparison. For example, the file type to compare. + Value string `json:"value" required:"true"` + + // TenantID is the UUID of the tenant who owns the rule in octavia. + // Only administrative users can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + + // The key to use for the comparison. For example, the name of the cookie to evaluate. + Key string `json:"key,omitempty"` + + // When true the logic of the rule is inverted. For example, with invert true, + // equal to would become not equal to. Default is false. + Invert bool `json:"invert,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToRuleCreateMap builds a request body from CreateRuleOpts. +func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "rule") +} + +// CreateRule will create and associate a Rule with a particular L7Policy. +func CreateRule(c *gophercloud.ServiceClient, policyID string, opts CreateRuleOpts) (r CreateRuleResult) { + b, err := opts.ToRuleCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(ruleRootURL(c, policyID), b, &r.Body, nil) + return +} + +// ListRulesOptsBuilder allows extensions to add additional parameters to the +// ListRules request. +type ListRulesOptsBuilder interface { + ToRulesListQuery() (string, error) +} + +// ListRulesOpts allows the filtering and sorting of paginated collections +// through the API. +type ListRulesOpts struct { + RuleType RuleType `q:"type"` + TenantID string `q:"tenant_id"` + CompareType CompareType `q:"compare_type"` + Value string `q:"value"` + Key string `q:"key"` + Invert bool `q:"invert"` + AdminStateUp bool `q:"admin_state_up"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToRulesListQuery formats a ListOpts into a query string. +func (opts ListRulesOpts) ToRulesListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListRules returns a Pager which allows you to iterate over a collection of +// rules. It accepts a ListRulesOptsBuilder, which allows you to filter and +// sort the returned collection for greater efficiency. +// +// Default policy settings return only those rules that are owned by the +// project who submits the request, unless an admin user submits the request. +func ListRules(c *gophercloud.ServiceClient, policyID string, opts ListRulesOptsBuilder) pagination.Pager { + url := ruleRootURL(c, policyID) + if opts != nil { + query, err := opts.ToRulesListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return RulePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// GetRule retrieves a particular L7Policy Rule based on its unique ID. +func GetRule(c *gophercloud.ServiceClient, policyID string, ruleID string) (r GetRuleResult) { + _, r.Err = c.Get(ruleResourceURL(c, policyID, ruleID), &r.Body, nil) + return +} + +// DeleteRule will remove a Rule from a particular L7Policy. +func DeleteRule(c *gophercloud.ServiceClient, policyID string, ruleID string) (r DeleteRuleResult) { + _, r.Err = c.Delete(ruleResourceURL(c, policyID, ruleID), nil) + return +} + +// UpdateRuleOptsBuilder allows to add additional parameters to the PUT request. +type UpdateRuleOptsBuilder interface { + ToRuleUpdateMap() (map[string]interface{}, error) +} + +// UpdateRuleOpts is the common options struct used in this package's Update +// operation. +type UpdateRuleOpts struct { + // The L7 rule type. One of COOKIE, FILE_TYPE, HEADER, HOST_NAME, or PATH. + RuleType RuleType `json:"type,omitempty"` + + // The comparison type for the L7 rule. One of CONTAINS, ENDS_WITH, EQUAL_TO, REGEX, or STARTS_WITH. + CompareType CompareType `json:"compare_type,omitempty"` + + // The value to use for the comparison. For example, the file type to compare. + Value string `json:"value,omitempty"` + + // The key to use for the comparison. For example, the name of the cookie to evaluate. + Key *string `json:"key,omitempty"` + + // When true the logic of the rule is inverted. For example, with invert true, + // equal to would become not equal to. Default is false. + Invert *bool `json:"invert,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToRuleUpdateMap builds a request body from UpdateRuleOpts. +func (opts UpdateRuleOpts) ToRuleUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "rule") + if err != nil { + return nil, err + } + + if m := b["rule"].(map[string]interface{}); m["key"] == "" { + m["key"] = nil + } + + return b, nil +} + +// UpdateRule allows Rule to be updated. +func UpdateRule(c *gophercloud.ServiceClient, policyID string, ruleID string, opts UpdateRuleOptsBuilder) (r UpdateRuleResult) { + b, err := opts.ToRuleUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(ruleResourceURL(c, policyID, ruleID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/l7policies/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/l7policies/results.go new file mode 100644 index 0000000000..5153b1b90c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/l7policies/results.go @@ -0,0 +1,245 @@ +package l7policies + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// L7Policy is a collection of L7 rules associated with a Listener, and which +// may also have an association to a back-end pool. +type L7Policy struct { + // The unique ID for the L7 policy. + ID string `json:"id"` + + // Name of the L7 policy. + Name string `json:"name"` + + // The ID of the listener. + ListenerID string `json:"listener_id"` + + // The L7 policy action. One of REDIRECT_TO_POOL, REDIRECT_TO_URL, or REJECT. + Action string `json:"action"` + + // The position of this policy on the listener. + Position int32 `json:"position"` + + // A human-readable description for the resource. + Description string `json:"description"` + + // TenantID is the UUID of the tenant who owns the L7 policy in octavia. + // Only administrative users can specify a project UUID other than their own. + TenantID string `json:"tenant_id"` + + // Requests matching this policy will be redirected to the pool with this ID. + // Only valid if action is REDIRECT_TO_POOL. + RedirectPoolID string `json:"redirect_pool_id"` + + // Requests matching this policy will be redirected to this URL. + // Only valid if action is REDIRECT_TO_URL. + RedirectURL string `json:"redirect_url"` + + // The administrative state of the L7 policy, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + + // The provisioning status of the L7 policy. + // This value is ACTIVE, PENDING_* or ERROR. + // This field seems to only be returned during a call to a load balancer's /status + // see: https://github.com/gophercloud/gophercloud/issues/1362 + ProvisioningStatus string `json:"provisioning_status"` + + // The operating status of the L7 policy. + // This field seems to only be returned during a call to a load balancer's /status + // see: https://github.com/gophercloud/gophercloud/issues/1362 + OperatingStatus string `json:"operating_status"` + + // Rules are List of associated L7 rule IDs. + Rules []Rule `json:"rules"` +} + +// Rule represents layer 7 load balancing rule. +type Rule struct { + // The unique ID for the L7 rule. + ID string `json:"id"` + + // The L7 rule type. One of COOKIE, FILE_TYPE, HEADER, HOST_NAME, or PATH. + RuleType string `json:"type"` + + // The comparison type for the L7 rule. One of CONTAINS, ENDS_WITH, EQUAL_TO, REGEX, or STARTS_WITH. + CompareType string `json:"compare_type"` + + // The value to use for the comparison. For example, the file type to compare. + Value string `json:"value"` + + // TenantID is the UUID of the tenant who owns the rule in octavia. + // Only administrative users can specify a project UUID other than their own. + TenantID string `json:"tenant_id"` + + // The key to use for the comparison. For example, the name of the cookie to evaluate. + Key string `json:"key"` + + // When true the logic of the rule is inverted. For example, with invert true, + // equal to would become not equal to. Default is false. + Invert bool `json:"invert"` + + // The administrative state of the L7 rule, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + + // The provisioning status of the L7 rule. + // This value is ACTIVE, PENDING_* or ERROR. + // This field seems to only be returned during a call to a load balancer's /status + // see: https://github.com/gophercloud/gophercloud/issues/1362 + ProvisioningStatus string `json:"provisioning_status"` + + // The operating status of the L7 policy. + // This field seems to only be returned during a call to a load balancer's /status + // see: https://github.com/gophercloud/gophercloud/issues/1362 + OperatingStatus string `json:"operating_status"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a l7policy. +func (r commonResult) Extract() (*L7Policy, error) { + var s struct { + L7Policy *L7Policy `json:"l7policy"` + } + err := r.ExtractInto(&s) + return s.L7Policy, err +} + +// CreateResult represents the result of a Create operation. Call its Extract +// method to interpret the result as a L7Policy. +type CreateResult struct { + commonResult +} + +// L7PolicyPage is the page returned by a pager when traversing over a +// collection of l7policies. +type L7PolicyPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of l7policies 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 L7PolicyPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"l7policies_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a L7PolicyPage struct is empty. +func (r L7PolicyPage) IsEmpty() (bool, error) { + is, err := ExtractL7Policies(r) + return len(is) == 0, err +} + +// ExtractL7Policies accepts a Page struct, specifically a L7PolicyPage struct, +// and extracts the elements into a slice of L7Policy structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractL7Policies(r pagination.Page) ([]L7Policy, error) { + var s struct { + L7Policies []L7Policy `json:"l7policies"` + } + err := (r.(L7PolicyPage)).ExtractInto(&s) + return s.L7Policies, err +} + +// GetResult represents the result of a Get operation. Call its Extract +// method to interpret the result as a L7Policy. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// UpdateResult represents the result of an Update operation. Call its Extract +// method to interpret the result as a L7Policy. +type UpdateResult struct { + commonResult +} + +type commonRuleResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a rule. +func (r commonRuleResult) Extract() (*Rule, error) { + var s struct { + Rule *Rule `json:"rule"` + } + err := r.ExtractInto(&s) + return s.Rule, err +} + +// CreateRuleResult represents the result of a CreateRule operation. +// Call its Extract method to interpret it as a Rule. +type CreateRuleResult struct { + commonRuleResult +} + +// RulePage is the page returned by a pager when traversing over a +// collection of Rules in a L7Policy. +type RulePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of rules 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 RulePage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"rules_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a RulePage struct is empty. +func (r RulePage) IsEmpty() (bool, error) { + is, err := ExtractRules(r) + return len(is) == 0, err +} + +// ExtractRules accepts a Page struct, specifically a RulePage struct, +// and extracts the elements into a slice of Rules structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractRules(r pagination.Page) ([]Rule, error) { + var s struct { + Rules []Rule `json:"rules"` + } + err := (r.(RulePage)).ExtractInto(&s) + return s.Rules, err +} + +// GetRuleResult represents the result of a GetRule operation. +// Call its Extract method to interpret it as a Rule. +type GetRuleResult struct { + commonRuleResult +} + +// DeleteRuleResult represents the result of a DeleteRule operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type DeleteRuleResult struct { + gophercloud.ErrResult +} + +// UpdateRuleResult represents the result of an UpdateRule operation. +// Call its Extract method to interpret it as a Rule. +type UpdateRuleResult struct { + commonRuleResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/l7policies/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/l7policies/urls.go new file mode 100644 index 0000000000..ecb607a8e8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/l7policies/urls.go @@ -0,0 +1,25 @@ +package l7policies + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "l7policies" + rulePath = "rules" +) + +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 ruleRootURL(c *gophercloud.ServiceClient, policyID string) string { + return c.ServiceURL(rootPath, resourcePath, policyID, rulePath) +} + +func ruleResourceURL(c *gophercloud.ServiceClient, policyID string, ruleID string) string { + return c.ServiceURL(rootPath, resourcePath, policyID, rulePath, ruleID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/doc.go new file mode 100644 index 0000000000..108cdb03d8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/doc.go @@ -0,0 +1,63 @@ +/* +Package listeners provides information and interaction with Listeners of the +LBaaS v2 extension for the OpenStack Networking service. + +Example to List Listeners + + listOpts := listeners.ListOpts{ + LoadbalancerID : "ca430f80-1737-4712-8dc6-3f640d55594b", + } + + allPages, err := listeners.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allListeners, err := listeners.ExtractListeners(allPages) + if err != nil { + panic(err) + } + + for _, listener := range allListeners { + fmt.Printf("%+v\n", listener) + } + +Example to Create a Listener + + createOpts := listeners.CreateOpts{ + Protocol: "TCP", + Name: "db", + LoadbalancerID: "79e05663-7f03-45d2-a092-8b94062f22ab", + AdminStateUp: gophercloud.Enabled, + DefaultPoolID: "41efe233-7591-43c5-9cf7-923964759f9e", + ProtocolPort: 3306, + } + + listener, err := listeners.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Listener + + listenerID := "d67d56a6-4a86-4688-a282-f46444705c64" + + i1001 := 1001 + updateOpts := listeners.UpdateOpts{ + ConnLimit: &i1001, + } + + listener, err := listeners.Update(networkClient, listenerID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Listener + + listenerID := "d67d56a6-4a86-4688-a282-f46444705c64" + err := listeners.Delete(networkClient, listenerID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package listeners 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 0000000000..f2966b6c44 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go @@ -0,0 +1,212 @@ +package listeners + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Type Protocol represents a listener protocol. +type Protocol string + +// Supported attributes for create/update operations. +const ( + ProtocolTCP Protocol = "TCP" + ProtocolHTTP Protocol = "HTTP" + ProtocolHTTPS Protocol = "HTTPS" + ProtocolTerminatedHTTPS Protocol = "TERMINATED_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"` + ProjectID string `q:"project_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 +// listeners. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those listeners 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 allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToListenerCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options for creating a listener. +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"` + + // TenantID is only required if the caller has an admin role and wants + // to create a pool for another project. + TenantID string `json:"tenant_id,omitempty"` + + // ProjectID is only required if the caller has an admin role and wants + // to create a pool for another project. + ProjectID string `json:"project_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 Barbican 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 builds a request body from CreateOpts. +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 allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToListenerUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents options for updating a Listener. +type UpdateOpts struct { + // 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 Barbican 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 builds a request body from UpdateOpts. +func (opts UpdateOpts) ToListenerUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "listener") + if err != nil { + return nil, err + } + + if m := b["listener"].(map[string]interface{}); m["default_pool_id"] == "" { + m["default_pool_id"] = nil + } + + return b, nil +} + +// 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 0000000000..ae10579322 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners/results.go @@ -0,0 +1,141 @@ +package listeners + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/l7policies" + "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. + 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"` + + // A reference to a Barbican 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 are the pools which are part of this listener. + Pools []pools.Pool `json:"pools"` + + // L7policies are the L7 policies which are part of this listener. + // This field seems to only be returned during a call to a load balancer's /status + // see: https://github.com/gophercloud/gophercloud/issues/1352 + L7Policies []l7policies.L7Policy `json:"l7policies"` + + // The provisioning status of the listener. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` +} + +// ListenerPage is the page returned by a pager when traversing over a +// collection of listeners. +type ListenerPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of listeners 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 ListenerPage 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 listener. +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. Call its Extract +// method to interpret it as a Listener. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Listener. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Listener. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +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 0000000000..02fb1eb39e --- /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/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/doc.go new file mode 100644 index 0000000000..c6d53a7b05 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/doc.go @@ -0,0 +1,79 @@ +/* +Package loadbalancers provides information and interaction with Load Balancers +of the LBaaS v2 extension for the OpenStack Networking service. + +Example to List Load Balancers + + listOpts := loadbalancers.ListOpts{ + Provider: "haproxy", + } + + allPages, err := loadbalancers.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allLoadbalancers, err := loadbalancers.ExtractLoadBalancers(allPages) + if err != nil { + panic(err) + } + + for _, lb := range allLoadbalancers { + fmt.Printf("%+v\n", lb) + } + +Example to Create a Load Balancer + + createOpts := loadbalancers.CreateOpts{ + Name: "db_lb", + AdminStateUp: gophercloud.Enabled, + VipSubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + VipAddress: "10.30.176.48", + Flavor: "medium", + Provider: "haproxy", + } + + lb, err := loadbalancers.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Load Balancer + + lbID := "d67d56a6-4a86-4688-a282-f46444705c64" + + i1001 := 1001 + updateOpts := loadbalancers.UpdateOpts{ + Name: "new-name", + } + + lb, err := loadbalancers.Update(networkClient, lbID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Load Balancers + + lbID := "d67d56a6-4a86-4688-a282-f46444705c64" + err := loadbalancers.Delete(networkClient, lbID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Get the Status of a Load Balancer + + lbID := "d67d56a6-4a86-4688-a282-f46444705c64" + status, err := loadbalancers.GetStatuses(networkClient, LBID).Extract() + if err != nil { + panic(err) + } + +Example to Get the Statistics of a Load Balancer + + lbID := "d67d56a6-4a86-4688-a282-f46444705c64" + stats, err := loadbalancers.GetStats(networkClient, LBID).Extract() + if err != nil { + panic(err) + } +*/ +package loadbalancers 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 0000000000..f5b1413482 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go @@ -0,0 +1,204 @@ +package loadbalancers + +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 { + 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"` + ProjectID string `q:"project_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 +// load balancers. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +// +// Default policy settings return only those load balancers 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 allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToLoadBalancerCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Human-readable name for the Loadbalancer. Does not have to be unique. + Name string `json:"name,omitempty"` + + // Human-readable description for the Loadbalancer. + Description string `json:"description,omitempty"` + + // 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"` + + // TenantID is the UUID of the project who owns the Loadbalancer. + // Only administrative users can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + + // ProjectID is the UUID of the project who owns the Loadbalancer. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // The IP address of the Loadbalancer. + VipAddress string `json:"vip_address,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // The UUID of a flavor. + Flavor string `json:"flavor,omitempty"` + + // The name of the provider. + Provider string `json:"provider,omitempty"` +} + +// ToLoadBalancerCreateMap builds a request body from CreateOpts. +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. +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 allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToLoadBalancerUpdateMap() (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 Loadbalancer. Does not have to be unique. + Name *string `json:"name,omitempty"` + + // Human-readable description for the Loadbalancer. + Description *string `json:"description,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToLoadBalancerUpdateMap builds a request body from UpdateOpts. +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 +} + +// CascadingDelete is like `Delete`, but will also delete any of the load balancer's +// children (listener, monitor, etc). +// NOTE: This function will only work with Octavia load balancers; Neutron does not +// support this. +func CascadingDelete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + if c.Type != "load-balancer" { + r.Err = fmt.Errorf("error prior to running cascade delete: only Octavia LBs supported") + return + } + u := fmt.Sprintf("%s?cascade=true", resourceURL(c, id)) + _, r.Err = c.Delete(u, nil) + return +} + +// GetStatuses will return the status of a particular LoadBalancer. +func GetStatuses(c *gophercloud.ServiceClient, id string) (r GetStatusesResult) { + _, r.Err = c.Get(statusRootURL(c, id), &r.Body, nil) + return +} + +// GetStats will return the shows the current statistics of a particular LoadBalancer. +func GetStats(c *gophercloud.ServiceClient, id string) (r StatsResult) { + _, r.Err = c.Get(statisticsRootURL(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 0000000000..7f423c933d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go @@ -0,0 +1,186 @@ +package loadbalancers + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools" + "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. + 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 are the listeners related to this Loadbalancer. + Listeners []listeners.Listener `json:"listeners"` + + // Pools are the pools related to this Loadbalancer. + Pools []pools.Pool `json:"pools"` +} + +// StatusTree represents the status of a loadbalancer. +type StatusTree struct { + Loadbalancer *LoadBalancer `json:"loadbalancer"` +} + +type Stats struct { + // The currently active connections. + ActiveConnections int `json:"active_connections"` + + // The total bytes received. + BytesIn int `json:"bytes_in"` + + // The total bytes sent. + BytesOut int `json:"bytes_out"` + + // The total requests that were unable to be fulfilled. + RequestErrors int `json:"request_errors"` + + // The total connections handled. + TotalConnections int `json:"total_connections"` +} + +// LoadBalancerPage is the page returned by a pager when traversing over a +// collection of load balancers. +type LoadBalancerPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of load balancers 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 (r LoadBalancerPage) IsEmpty() (bool, error) { + is, err := ExtractLoadBalancers(r) + 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 loadbalancer. +func (r commonResult) Extract() (*LoadBalancer, error) { + var s struct { + LoadBalancer *LoadBalancer `json:"loadbalancer"` + } + err := r.ExtractInto(&s) + return s.LoadBalancer, err +} + +// GetStatusesResult represents the result of a GetStatuses operation. +// Call its Extract method to interpret it as a StatusTree. +type GetStatusesResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts the status of +// a Loadbalancer. +func (r GetStatusesResult) Extract() (*StatusTree, error) { + var s struct { + Statuses *StatusTree `json:"statuses"` + } + err := r.ExtractInto(&s) + return s.Statuses, err +} + +// StatsResult represents the result of a GetStats operation. +// Call its Extract method to interpret it as a Stats. +type StatsResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts the status of +// a Loadbalancer. +func (r StatsResult) Extract() (*Stats, error) { + var s struct { + Stats *Stats `json:"stats"` + } + err := r.ExtractInto(&s) + return s.Stats, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a LoadBalancer. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a LoadBalancer. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a LoadBalancer. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +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 0000000000..2d2a99b779 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/urls.go @@ -0,0 +1,26 @@ +package loadbalancers + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "loadbalancers" + statusPath = "statuses" + statisticsPath = "stats" +) + +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) +} + +func statisticsRootURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, statisticsPath) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/doc.go new file mode 100644 index 0000000000..6ed8c8fb5f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/doc.go @@ -0,0 +1,69 @@ +/* +Package monitors provides information and interaction with Monitors +of the LBaaS v2 extension for the OpenStack Networking service. + +Example to List Monitors + + listOpts := monitors.ListOpts{ + PoolID: "c79a4468-d788-410c-bf79-9a8ef6354852", + } + + allPages, err := monitors.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allMonitors, err := monitors.ExtractMonitors(allPages) + if err != nil { + panic(err) + } + + for _, monitor := range allMonitors { + fmt.Printf("%+v\n", monitor) + } + +Example to Create a Monitor + + createOpts := monitors.CreateOpts{ + Type: "HTTP", + Name: "db", + PoolID: "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d", + Delay: 20, + Timeout: 10, + MaxRetries: 5, + URLPath: "/check", + ExpectedCodes: "200-299", + } + + monitor, err := monitors.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Monitor + + monitorID := "d67d56a6-4a86-4688-a282-f46444705c64" + + updateOpts := monitors.UpdateOpts{ + Name: "NewHealthmonitorName", + Delay: 3, + Timeout: 20, + MaxRetries: 10, + URLPath: "/another_check", + ExpectedCodes: "301", + } + + monitor, err := monitors.Update(networkClient, monitorID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Monitor + + monitorID := "d67d56a6-4a86-4688-a282-f46444705c64" + err := monitors.Delete(networkClient, monitorID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package monitors 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 0000000000..f728f5a823 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go @@ -0,0 +1,257 @@ +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"` + ProjectID string `q:"project_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 allows extensions to add additional parameters to the +// List request. +type CreateOptsBuilder interface { + ToMonitorCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // The Pool to Monitor. + PoolID string `json:"pool_id" required:"true"` + + // 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"` + + // The time, in seconds, between sending probes to members. + Delay int `json:"delay" required:"true"` + + // 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"` + + // 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"` + + // URI path that will be accessed if Monitor type is HTTP or HTTPS. + // Required for HTTP(S) types. + URLPath string `json:"url_path,omitempty"` + + // The HTTP method used for requests by the Monitor. If this attribute + // is not specified, it defaults to "GET". Required for HTTP(S) types. + HTTPMethod string `json:"http_method,omitempty"` + + // Expected HTTP codes for a passing HTTP(S) Monitor. You can either specify + // a single status like "200", or a range like "200-202". Required for HTTP(S) + // types. + ExpectedCodes string `json:"expected_codes,omitempty"` + + // TenantID is the UUID of the project who owns the Monitor. + // Only administrative users can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + + // ProjectID is the UUID of the project who owns the Monitor. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // The Name of the Monitor. + Name string `json:"name,omitempty"` + + // The administrative state of the Monitor. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToMonitorCreateMap builds a request body from CreateOpts. +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 allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToMonitorUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // The time, in seconds, between sending probes to members. + Delay int `json:"delay,omitempty"` + + // 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"` + + // 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"` + + // URI path that will be accessed if Monitor type is HTTP or HTTPS. + // Required for HTTP(S) types. + URLPath string `json:"url_path,omitempty"` + + // The HTTP method used for requests by the Monitor. If this attribute + // is not specified, it defaults to "GET". Required for HTTP(S) types. + HTTPMethod string `json:"http_method,omitempty"` + + // Expected HTTP codes for a passing HTTP(S) Monitor. You can either specify + // a single status like "200", or a range like "200-202". Required for HTTP(S) + // types. + ExpectedCodes string `json:"expected_codes,omitempty"` + + // The Name of the Monitor. + Name *string `json:"name,omitempty"` + + // The administrative state of the Monitor. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToMonitorUpdateMap builds a request body from UpdateOpts. +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 0000000000..a78f7aeb0f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go @@ -0,0 +1,153 @@ +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"` + + // TenantID is the owner of the Monitor. + 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"` + + // The provisioning status of the monitor. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` +} + +// 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. Call its Extract +// method to interpret it as a Monitor. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Monitor. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Monitor. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the result succeeded or failed. +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 0000000000..a222e52a93 --- /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/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/doc.go new file mode 100644 index 0000000000..0697148680 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/doc.go @@ -0,0 +1,126 @@ +/* +Package pools provides information and interaction with Pools and +Members of the LBaaS v2 extension for the OpenStack Networking service. + +Example to List Pools + + listOpts := pools.ListOpts{ + LoadbalancerID: "c79a4468-d788-410c-bf79-9a8ef6354852", + } + + allPages, err := pools.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allPools, err := pools.ExtractPools(allPages) + if err != nil { + panic(err) + } + + for _, pools := range allPools { + fmt.Printf("%+v\n", pool) + } + +Example to Create a Pool + + createOpts := pools.CreateOpts{ + LBMethod: pools.LBMethodRoundRobin, + Protocol: "HTTP", + Name: "Example pool", + LoadbalancerID: "79e05663-7f03-45d2-a092-8b94062f22ab", + } + + pool, err := pools.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Pool + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + + updateOpts := pools.UpdateOpts{ + Name: "new-name", + } + + pool, err := pools.Update(networkClient, poolID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Pool + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + err := pools.Delete(networkClient, poolID).ExtractErr() + if err != nil { + panic(err) + } + +Example to List Pool Members + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + + listOpts := pools.ListMemberOpts{ + ProtocolPort: 80, + } + + allPages, err := pools.ListMembers(networkClient, poolID, listOpts).AllPages() + if err != nil { + panic(err) + } + + allMembers, err := pools.ExtractMembers(allPages) + if err != nil { + panic(err) + } + + for _, member := allMembers { + fmt.Printf("%+v\n", member) + } + +Example to Create a Member + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + + weight := 10 + createOpts := pools.CreateMemberOpts{ + Name: "db", + SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9", + Address: "10.0.2.11", + ProtocolPort: 80, + Weight: &weight, + } + + member, err := pools.CreateMember(networkClient, poolID, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Member + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + memberID := "64dba99f-8af8-4200-8882-e32a0660f23e" + + weight := 4 + updateOpts := pools.UpdateMemberOpts{ + Name: "new-name", + Weight: &weight, + } + + member, err := pools.UpdateMember(networkClient, poolID, memberID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Member + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + memberID := "64dba99f-8af8-4200-8882-e32a0660f23e" + + err := pools.DeleteMember(networkClient, poolID, memberID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package pools 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 0000000000..f427ae7bf5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go @@ -0,0 +1,356 @@ +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"` + ProjectID string `q:"project_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 allows extensions to add additional parameters to the +// Create request. +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"` + + // TenantID is the UUID of the project who owns the Pool. + // Only administrative users can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + + // ProjectID is the UUID of the project who owns the Pool. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // Name of the pool. + Name string `json:"name,omitempty"` + + // Human-readable description for the pool. + Description string `json:"description,omitempty"` + + // Persistence is the session persistence of the pool. + // 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 builds a request body from CreateOpts. +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 allows extensions to add additional parameters to the +// Update request. +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 builds a request body from UpdateOpts. +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 allows extensions to add additional parameters to the +// CreateMember request. +type CreateMemberOptsBuilder interface { + ToMemberCreateMap() (map[string]interface{}, error) +} + +// CreateMemberOpts is the common options struct used in this package's CreateMember +// operation. +type CreateMemberOpts struct { + // The IP address of the member to receive traffic from the load balancer. + Address string `json:"address" required:"true"` + + // The port on which to listen for client traffic. + ProtocolPort int `json:"protocol_port" required:"true"` + + // Name of the Member. + Name string `json:"name,omitempty"` + + // TenantID is the UUID of the project who owns the Member. + // Only administrative users can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + + // ProjectID is the UUID of the project who owns the Member. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // 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"` + + // If you omit this parameter, LBaaS uses the vip_subnet_id parameter value + // for the subnet UUID. + SubnetID string `json:"subnet_id,omitempty"` + + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToMemberCreateMap builds a request body from CreateMemberOpts. +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 +} + +// UpdateMemberOptsBuilder allows extensions to add additional parameters to the +// List request. +type UpdateMemberOptsBuilder interface { + ToMemberUpdateMap() (map[string]interface{}, error) +} + +// UpdateMemberOpts is the common options struct used in this package's Update +// operation. +type UpdateMemberOpts struct { + // Name of the Member. + Name *string `json:"name,omitempty"` + + // 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"` + + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToMemberUpdateMap builds a request body from UpdateMemberOpts. +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 0000000000..fba0d3a878 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools/results.go @@ -0,0 +1,291 @@ +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"` +} + +// LoadBalancerID represents a load balancer. +type LoadBalancerID struct { + ID string `json:"id"` +} + +// ListenerID represents a listener. +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. + 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 load balancer provider. + Provider string `json:"provider"` + + // The Monitor associated with this Pool. + Monitor monitors.Monitor `json:"healthmonitor"` + + // The provisioning status of the pool. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // The operating status of the pool. + // This field seems to only be returned during a call to a load balancer's /status + // see: https://github.com/gophercloud/gophercloud/issues/1362 + OperatingStatus string `json:"operating_status"` +} + +// 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 Pool 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 pool. +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. Call its Extract +// method to interpret the result as a Pool. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation. Call its Extract +// method to interpret the result as a Pool. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation. Call its Extract +// method to interpret the result as a Pool. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +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. + 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"` + + // The provisioning status of the member. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // The operating status of the member. + // This field seems to only be returned during a call to a load balancer's /status + // see: https://github.com/gophercloud/gophercloud/issues/1362 + OperatingStatus string `json:"operating_status"` +} + +// 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 MemberPage struct, +// and extracts the elements into a slice of Members 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 member. +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. +// Call its Extract method to interpret it as a Member. +type CreateMemberResult struct { + commonMemberResult +} + +// GetMemberResult represents the result of a GetMember operation. +// Call its Extract method to interpret it as a Member. +type GetMemberResult struct { + commonMemberResult +} + +// UpdateMemberResult represents the result of an UpdateMember operation. +// Call its Extract method to interpret it as a Member. +type UpdateMemberResult struct { + commonMemberResult +} + +// DeleteMemberResult represents the result of a DeleteMember operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +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 0000000000..e7443c4f19 --- /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, memberID string) string { + return c.ServiceURL(rootPath, resourcePath, poolID, memberPath, memberID) +}