
Adds GCEPersistentDisk volume struct Adds gce-utils to attach disk to kubelet's VM. Updates config to give compute-rw to every minion. Adds GCEPersistentDisk to API Adds ability to mount attached disks Generalizes PD and adds tests. PD now uses an pluggable API interface. Unit Tests more cleanly separates TearDown and SetUp Modify boilerplate hook to omit build tags Adds Mounter interface; mount is now built by OS TearDown() for PD now detaches disk on final refcount Un-generalized PD; GCE calls moved to cloudprovider Address comments.
388 lines
11 KiB
Go
388 lines
11 KiB
Go
/*
|
|
Copyright 2014 Google Inc. All rights reserved.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package gce_cloud
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"os/exec"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.google.com/p/goauth2/compute/serviceaccount"
|
|
compute "code.google.com/p/google-api-go-client/compute/v1"
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider"
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/resources"
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
|
"github.com/golang/glog"
|
|
)
|
|
|
|
// GCECloud is an implementation of Interface, TCPLoadBalancer and Instances for Google Compute Engine.
|
|
type GCECloud struct {
|
|
service *compute.Service
|
|
projectID string
|
|
zone string
|
|
instanceID string
|
|
}
|
|
|
|
func init() {
|
|
cloudprovider.RegisterCloudProvider("gce", func(config io.Reader) (cloudprovider.Interface, error) { return newGCECloud() })
|
|
}
|
|
|
|
func getMetadata(url string) (string, error) {
|
|
client := http.Client{}
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Add("X-Google-Metadata-Request", "True")
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer res.Body.Close()
|
|
data, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
func getProjectAndZone() (string, string, error) {
|
|
url := "http://metadata/computeMetadata/v1/instance/zone"
|
|
result, err := getMetadata(url)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
parts := strings.Split(result, "/")
|
|
if len(parts) != 4 {
|
|
return "", "", fmt.Errorf("Unexpected response: %s", result)
|
|
}
|
|
return parts[1], parts[3], nil
|
|
}
|
|
|
|
func getInstanceID() (string, error) {
|
|
url := "http://metadata/computeMetadata/v1/instance/hostname"
|
|
result, err := getMetadata(url)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
parts := strings.Split(result, ".")
|
|
if len(parts) == 0 {
|
|
return "", fmt.Errorf("Unexpected response: %s", result)
|
|
}
|
|
return parts[0], nil
|
|
}
|
|
|
|
// newGCECloud creates a new instance of GCECloud.
|
|
func newGCECloud() (*GCECloud, error) {
|
|
projectID, zone, err := getProjectAndZone()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// TODO: if we want to use this on a machine that doesn't have the http://metadata server
|
|
// e.g. on a user's machine (not VM) somewhere, we need to have an alternative for
|
|
// instance id lookup.
|
|
instanceID, err := getInstanceID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
client, err := serviceaccount.NewClient(&serviceaccount.Options{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
svc, err := compute.New(client)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &GCECloud{
|
|
service: svc,
|
|
projectID: projectID,
|
|
zone: zone,
|
|
instanceID: instanceID,
|
|
}, nil
|
|
}
|
|
|
|
// TCPLoadBalancer returns an implementation of TCPLoadBalancer for Google Compute Engine.
|
|
func (gce *GCECloud) TCPLoadBalancer() (cloudprovider.TCPLoadBalancer, bool) {
|
|
return gce, true
|
|
}
|
|
|
|
// Instances returns an implementation of Instances for Google Compute Engine.
|
|
func (gce *GCECloud) Instances() (cloudprovider.Instances, bool) {
|
|
return gce, true
|
|
}
|
|
|
|
// Zones returns an implementation of Zones for Google Compute Engine.
|
|
func (gce *GCECloud) Zones() (cloudprovider.Zones, bool) {
|
|
return gce, true
|
|
}
|
|
|
|
func makeHostLink(projectID, zone, host string) string {
|
|
host = canonicalizeInstanceName(host)
|
|
return fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/zones/%s/instances/%s",
|
|
projectID, zone, host)
|
|
}
|
|
|
|
func (gce *GCECloud) makeTargetPool(name, region string, hosts []string) (string, error) {
|
|
var instances []string
|
|
for _, host := range hosts {
|
|
instances = append(instances, makeHostLink(gce.projectID, gce.zone, host))
|
|
}
|
|
pool := &compute.TargetPool{
|
|
Name: name,
|
|
Instances: instances,
|
|
}
|
|
_, err := gce.service.TargetPools.Insert(gce.projectID, region, pool).Do()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
link := fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/regions/%s/targetPools/%s", gce.projectID, region, name)
|
|
return link, nil
|
|
}
|
|
|
|
func (gce *GCECloud) waitForRegionOp(op *compute.Operation, region string) error {
|
|
pollOp := op
|
|
for pollOp.Status != "DONE" {
|
|
var err error
|
|
time.Sleep(time.Second * 10)
|
|
pollOp, err = gce.service.RegionOperations.Get(gce.projectID, region, op.Name).Do()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TCPLoadBalancerExists is an implementation of TCPLoadBalancer.TCPLoadBalancerExists.
|
|
func (gce *GCECloud) TCPLoadBalancerExists(name, region string) (bool, error) {
|
|
_, err := gce.service.ForwardingRules.Get(gce.projectID, region, name).Do()
|
|
return false, err
|
|
}
|
|
|
|
// CreateTCPLoadBalancer is an implementation of TCPLoadBalancer.CreateTCPLoadBalancer.
|
|
func (gce *GCECloud) CreateTCPLoadBalancer(name, region string, port int, hosts []string) error {
|
|
pool, err := gce.makeTargetPool(name, region, hosts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req := &compute.ForwardingRule{
|
|
Name: name,
|
|
IPProtocol: "TCP",
|
|
PortRange: strconv.Itoa(port),
|
|
Target: pool,
|
|
}
|
|
_, err = gce.service.ForwardingRules.Insert(gce.projectID, region, req).Do()
|
|
return err
|
|
}
|
|
|
|
// UpdateTCPLoadBalancer is an implementation of TCPLoadBalancer.UpdateTCPLoadBalancer.
|
|
func (gce *GCECloud) UpdateTCPLoadBalancer(name, region string, hosts []string) error {
|
|
var refs []*compute.InstanceReference
|
|
for _, host := range hosts {
|
|
refs = append(refs, &compute.InstanceReference{host})
|
|
}
|
|
req := &compute.TargetPoolsAddInstanceRequest{
|
|
Instances: refs,
|
|
}
|
|
|
|
_, err := gce.service.TargetPools.AddInstance(gce.projectID, region, name, req).Do()
|
|
return err
|
|
}
|
|
|
|
// DeleteTCPLoadBalancer is an implementation of TCPLoadBalancer.DeleteTCPLoadBalancer.
|
|
func (gce *GCECloud) DeleteTCPLoadBalancer(name, region string) error {
|
|
_, err := gce.service.ForwardingRules.Delete(gce.projectID, region, name).Do()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = gce.service.TargetPools.Delete(gce.projectID, region, name).Do()
|
|
return err
|
|
}
|
|
|
|
// Take a GCE instance 'hostname' and break it down to something that can be fed
|
|
// to the GCE API client library. Basically this means reducing 'kubernetes-
|
|
// minion-2.c.my-proj.internal' to 'kubernetes-minion-2' if necessary.
|
|
func canonicalizeInstanceName(name string) string {
|
|
ix := strings.Index(name, ".")
|
|
if ix != -1 {
|
|
name = name[:ix]
|
|
}
|
|
return name
|
|
}
|
|
|
|
// IPAddress is an implementation of Instances.IPAddress.
|
|
func (gce *GCECloud) IPAddress(instance string) (net.IP, error) {
|
|
instance = canonicalizeInstanceName(instance)
|
|
res, err := gce.service.Instances.Get(gce.projectID, gce.zone, instance).Do()
|
|
if err != nil {
|
|
glog.Errorf("Failed to retrieve TargetInstance resource for instance:%s", instance)
|
|
return nil, err
|
|
}
|
|
ip := net.ParseIP(res.NetworkInterfaces[0].AccessConfigs[0].NatIP)
|
|
if ip == nil {
|
|
return nil, fmt.Errorf("Invalid network IP: %s", res.NetworkInterfaces[0].AccessConfigs[0].NatIP)
|
|
}
|
|
return ip, nil
|
|
}
|
|
|
|
// fqdnSuffix is hacky function to compute the delta between hostame and hostname -f.
|
|
func fqdnSuffix() (string, error) {
|
|
fullHostname, err := exec.Command("hostname", "-f").Output()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hostname, err := exec.Command("hostname").Output()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(string(fullHostname)[len(string(hostname)):]), nil
|
|
}
|
|
|
|
// List is an implementation of Instances.List.
|
|
func (gce *GCECloud) List(filter string) ([]string, error) {
|
|
// GCE gives names without their fqdn suffix, so get that here for appending.
|
|
// This is needed because the kubelet looks for its jobs in /registry/hosts/<fqdn>/pods
|
|
// We should really just replace this convention, with a negotiated naming protocol for kubelet's
|
|
// to register with the master.
|
|
suffix, err := fqdnSuffix()
|
|
if err != nil {
|
|
return []string{}, err
|
|
}
|
|
if len(suffix) > 0 {
|
|
suffix = "." + suffix
|
|
}
|
|
listCall := gce.service.Instances.List(gce.projectID, gce.zone)
|
|
if len(filter) > 0 {
|
|
listCall = listCall.Filter("name eq " + filter)
|
|
}
|
|
res, err := listCall.Do()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var instances []string
|
|
for _, instance := range res.Items {
|
|
instances = append(instances, instance.Name+suffix)
|
|
}
|
|
return instances, nil
|
|
}
|
|
|
|
func makeResources(cpu float32, memory float32) *api.NodeResources {
|
|
return &api.NodeResources{
|
|
Capacity: api.ResourceList{
|
|
resources.CPU: util.NewIntOrStringFromInt(int(cpu * 1000)),
|
|
resources.Memory: util.NewIntOrStringFromInt(int(memory * 1024 * 1024 * 1024)),
|
|
},
|
|
}
|
|
}
|
|
|
|
func canonicalizeMachineType(machineType string) string {
|
|
ix := strings.LastIndex(machineType, "/")
|
|
return machineType[ix+1:]
|
|
}
|
|
|
|
func (gce *GCECloud) GetNodeResources(name string) (*api.NodeResources, error) {
|
|
instance := canonicalizeInstanceName(name)
|
|
instanceCall := gce.service.Instances.Get(gce.projectID, gce.zone, instance)
|
|
res, err := instanceCall.Do()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch canonicalizeMachineType(res.MachineType) {
|
|
case "f1-micro":
|
|
return makeResources(1, 0.6), nil
|
|
case "g1-small":
|
|
return makeResources(1, 1.70), nil
|
|
case "n1-standard-1":
|
|
return makeResources(1, 3.75), nil
|
|
case "n1-standard-2":
|
|
return makeResources(2, 7.5), nil
|
|
case "n1-standard-4":
|
|
return makeResources(4, 15), nil
|
|
case "n1-standard-8":
|
|
return makeResources(8, 30), nil
|
|
case "n1-standard-16":
|
|
return makeResources(16, 30), nil
|
|
default:
|
|
glog.Errorf("unknown machine: %s", res.MachineType)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
func (gce *GCECloud) GetZone() (cloudprovider.Zone, error) {
|
|
region, err := getGceRegion(gce.zone)
|
|
if err != nil {
|
|
return cloudprovider.Zone{}, err
|
|
}
|
|
return cloudprovider.Zone{
|
|
FailureDomain: gce.zone,
|
|
Region: region,
|
|
}, nil
|
|
}
|
|
|
|
func (gce *GCECloud) AttachDisk(diskName string, readOnly bool) error {
|
|
disk, err := gce.getDisk(diskName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
readWrite := "READ_WRITE"
|
|
if readOnly {
|
|
readWrite = "READ_ONLY"
|
|
}
|
|
attachedDisk := gce.convertDiskToAttachedDisk(disk, readWrite)
|
|
_, err = gce.service.Instances.AttachDisk(gce.projectID, gce.zone, gce.instanceID, attachedDisk).Do()
|
|
return err
|
|
}
|
|
|
|
func (gce *GCECloud) DetachDisk(devicePath string) error {
|
|
_, err := gce.service.Instances.DetachDisk(gce.projectID, gce.zone, gce.instanceID, devicePath).Do()
|
|
return err
|
|
}
|
|
|
|
func (gce *GCECloud) getDisk(diskName string) (*compute.Disk, error) {
|
|
return gce.service.Disks.Get(gce.projectID, gce.zone, diskName).Do()
|
|
}
|
|
|
|
// getGceRegion returns region of the gce zone. Zone names
|
|
// are of the form: ${region-name}-${ix}.
|
|
// For example "us-central1-b" has a region of "us-central1".
|
|
// So we look for the last '-' and trim to just before that.
|
|
func getGceRegion(zone string) (string, error) {
|
|
ix := strings.LastIndex(zone, "-")
|
|
if ix == -1 {
|
|
return "", fmt.Errorf("unexpected zone: %s", zone)
|
|
}
|
|
return zone[:ix], nil
|
|
}
|
|
|
|
// Converts a Disk resource to an AttachedDisk resource.
|
|
func (gce *GCECloud) convertDiskToAttachedDisk(disk *compute.Disk, readWrite string) *compute.AttachedDisk {
|
|
return &compute.AttachedDisk{
|
|
DeviceName: disk.Name,
|
|
Kind: disk.Kind,
|
|
Mode: readWrite,
|
|
Source: "https://" + path.Join("www.googleapis.com/compute/v1/projects/", gce.projectID, "zones", gce.zone, "disks", disk.Name),
|
|
Type: "PERSISTENT",
|
|
}
|
|
}
|