docs/backends/gce.go

381 lines
11 KiB
Go

//
// Copyright (C) 2013 The Docker Cloud 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 backends
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"os/user"
"path"
"strings"
"time"
"code.google.com/p/goauth2/oauth"
compute "code.google.com/p/google-api-go-client/compute/v1"
)
var (
gceDefaultClientID = "676599397109-0te3n95co16j9mkinnq6vdhphp4nnd06.apps.googleusercontent.com"
gceDefaultClientSecret = "JnMnI5z9iH7YItv_jy_TZ1Hg"
gceDefaultScope = "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/compute https://www.googleapis.com/auth/devstorage.read_write"
gceInstanceType = flag.String("instancetype",
"/zones/us-central1-a/machineTypes/n1-standard-1",
"The reference to the instance type to create.")
gceImage = flag.String("image",
"https://www.googleapis.com/compute/v1/projects/google-containers/global/images/container-vm-v20140522",
"The GCE image to boot from.")
gceDiskName = flag.String("diskname", "docker-root", "Name of the instance root disk")
gceDiskSizeGb = flag.Int64("disksize", 100, "Size of the root disk in GB")
)
// A Google Compute Engine implementation of the Cloud interface
type GCECloud struct {
service *compute.Service
projectId string
}
type gcloudCredentialsCache struct {
Data []gceConfig
}
type gceConfig struct {
Credential gceCredential
Key gceKey
ProjectId string `json:"projectId"`
}
type gceCredential struct {
ClientId string `json:"Client_Id"`
ClientSecret string `json:"Client_Secret"`
RefreshToken string `json:"Refresh_Token"`
}
type gceKey struct {
Scope string
}
func gceConfAbsPath() (string, error) {
usr, err := user.Current()
if err != nil {
return "", err
}
return path.Join(usr.HomeDir, "docker-cloud.json"), nil
}
func gcloudConfAbsPath() (string, error) {
usr, err := user.Current()
if err != nil {
return "", err
}
return path.Join(usr.HomeDir, ".config/gcloud/credentials"), nil
}
func (conf *gceConfig) Read() (err error) {
if err = conf.readDockerCloud(); err == nil {
return
}
if err = conf.readGCloud(); err == nil {
return
}
return
}
func (conf *gceConfig) readDockerCloud() (err error) {
confPath, err := gceConfAbsPath()
if err != nil {
return
}
var data []byte
if data, err = ioutil.ReadFile(confPath); err != nil {
return
}
err = json.Unmarshal(data, conf)
return
}
func (conf *gceConfig) readGCloud() error {
confPath, err := gcloudConfAbsPath()
if err != nil {
return err
}
f, err := os.Open(confPath)
if err != nil {
return fmt.Errorf("unable to load gcloud credentials: %q", confPath)
}
defer f.Close()
cache := &gcloudCredentialsCache{}
if err := json.NewDecoder(f).Decode(cache); err != nil {
return err
}
if len(cache.Data) == 0 {
return fmt.Errorf("no gcloud credentials cached in: %q", confPath)
}
*conf = cache.Data[0]
return nil
}
func (conf *gceConfig) Write() error {
confPath, err := gceConfAbsPath()
if err != nil {
return err
}
var data []byte
if data, err = json.Marshal(conf); err != nil {
return err
}
return ioutil.WriteFile(confPath, data, 0644)
}
// Create a GCE Cloud instance. 'clientId', 'clientSecret' and 'scope' are used to ask for a client
// credential. 'code' is optional and is only used if a cached credential can not be found.
// 'projectId' is the Google Cloud project name.
func NewCloudGCE(projectId string) (cloud *GCECloud, err error) {
conf := &gceConfig{}
if err = conf.Read(); err != nil || conf.Credential.RefreshToken == "" {
return nil, errors.New("Did you authorize the client? Run `docker-cloud auth`.")
}
if projectId == "" {
if conf.ProjectId == "" {
return nil, errors.New("Did you define project id? Run `docker-cloud start -project=<project-id>`")
}
projectId = conf.ProjectId
}
oAuth2Conf := newGCEOAuth2Config(conf.Credential.ClientId, conf.Credential.ClientSecret, conf.Key.Scope)
transport := &oauth.Transport{
Config: oAuth2Conf,
// Make the actual request using the cached token to authenticate.
// ("Here's the token, let me in!")
Token: &oauth.Token{RefreshToken: conf.Credential.RefreshToken},
Transport: http.DefaultTransport,
}
// TODO(jbd): Does it need to refresh the token, transport will auto do it if
// it fails with an auth error on the first request.
err = transport.Refresh()
if err != nil {
return
}
svc, err := compute.New(transport.Client())
if err != nil {
return
}
return &GCECloud{
service: svc,
projectId: projectId,
}, nil
}
// Implementation of the Cloud interface
func (cloud GCECloud) GetPublicIPAddress(name string, zone string) (string, error) {
instance, err := cloud.service.Instances.Get(cloud.projectId, zone, name).Do()
if err != nil {
return "", err
}
// Found the instance, we're good.
return instance.NetworkInterfaces[0].AccessConfigs[0].NatIP, nil
}
// Get or create a new root disk.
func (cloud GCECloud) getOrCreateRootDisk(name, zone string) (string, error) {
log.Printf("try getting root disk: %q", name)
disk, err := cloud.service.Disks.Get(cloud.projectId, zone, *gceDiskName).Do()
if err == nil {
log.Printf("found %q", disk.SelfLink)
return disk.SelfLink, nil
}
log.Printf("not found, creating root disk: %q", name)
op, err := cloud.service.Disks.Insert(cloud.projectId, zone, &compute.Disk{
Name: *gceDiskName,
}).SourceImage(*gceImage).Do()
if err != nil {
log.Printf("disk insert api call failed: %v", err)
return "", err
}
err = cloud.waitForOp(op, zone)
if err != nil {
log.Printf("disk insert operation failed: %v", err)
return "", err
}
log.Printf("root disk created: %q", op.TargetLink)
return op.TargetLink, nil
}
// Implementation of the Cloud interface
func (cloud GCECloud) CreateInstance(name string, zone string) (string, error) {
rootDisk, err := cloud.getOrCreateRootDisk(*gceDiskName, zone)
if err != nil {
log.Printf("failed to create root disk: %v", err)
return "", err
}
prefix := "https://www.googleapis.com/compute/v1/projects/" + cloud.projectId
instance := &compute.Instance{
Name: name,
Description: "Docker on GCE",
MachineType: prefix + *gceInstanceType,
Disks: []*compute.AttachedDisk{
{
Boot: true,
Type: "PERSISTENT",
Mode: "READ_WRITE",
Source: rootDisk,
},
},
NetworkInterfaces: []*compute.NetworkInterface{
{
AccessConfigs: []*compute.AccessConfig{
{Type: "ONE_TO_ONE_NAT"},
},
Network: prefix + "/global/networks/default",
},
},
}
log.Printf("starting instance: %q", name)
op, err := cloud.service.Instances.Insert(cloud.projectId, zone, instance).Do()
if err != nil {
log.Printf("instance insert api call failed: %v", err)
return "", err
}
err = cloud.waitForOp(op, zone)
if err != nil {
log.Printf("instance insert operation failed: %v", err)
return "", err
}
// Wait for docker to come up
// TODO(bburns) : Use metadata instead to signal that docker is up and read.
time.Sleep(60 * time.Second)
log.Printf("instance started: %q", instance.NetworkInterfaces[0].AccessConfigs[0].NatIP)
return instance.NetworkInterfaces[0].AccessConfigs[0].NatIP, err
}
// Implementation of the Cloud interface
func (cloud GCECloud) DeleteInstance(name string, zone string) error {
op, err := cloud.service.Instances.Delete(cloud.projectId, zone, name).Do()
if err != nil {
log.Printf("Got compute.Operation, err: %#v, %v", op, err)
return err
}
return cloud.waitForOp(op, zone)
}
func (cloud GCECloud) OpenSecureTunnel(name, zone string, localPort, remotePort int) (*os.Process, error) {
return cloud.openSecureTunnel(name, zone, "localhost", localPort, remotePort)
}
func (cloud GCECloud) openSecureTunnel(name, zone, hostname string, localPort, remotePort int) (*os.Process, error) {
ip, err := cloud.GetPublicIPAddress(name, zone)
if err != nil {
return nil, err
}
username := os.Getenv("USER")
homedir := os.Getenv("HOME")
sshCommand := fmt.Sprintf("-o LogLevel=quiet -o UserKnownHostsFile=/dev/null -o CheckHostIP=no -o StrictHostKeyChecking=no -i %s/.ssh/google_compute_engine -A -p 22 %s@%s -f -N -L %d:%s:%d", homedir, username, ip, localPort, hostname, remotePort)
log.Printf("Running %s", sshCommand)
cmd := exec.Command("ssh", strings.Split(sshCommand, " ")...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
return cmd.Process, nil
}
// Wait for a compute operation to finish.
// op The operation
// zone The zone for the operation
// Returns an error if one occurs, or nil
func (cloud GCECloud) waitForOp(op *compute.Operation, zone string) error {
op, err := cloud.service.ZoneOperations.Get(cloud.projectId, zone, op.Name).Do()
for op.Status != "DONE" {
time.Sleep(5 * time.Second)
op, err = cloud.service.ZoneOperations.Get(cloud.projectId, zone, op.Name).Do()
if err != nil {
log.Printf("Got compute.Operation, err: %#v, %v", op, err)
}
if op.Status != "PENDING" && op.Status != "RUNNING" && op.Status != "DONE" {
log.Printf("Error waiting for operation: %s\n", op)
return errors.New(fmt.Sprintf("Bad operation: %s", op))
}
}
return err
}
func ConfigureGCE(clientId, clientSecret, scope, projectId string) error {
// Set up a configuration.
config := newGCEOAuth2Config(clientId, clientSecret, scope)
// Set up a Transport using the config.
// transport := &oauth.Transport{Config: config,
// Transport: &LogTransport{http.DefaultTransport},}
transport := &oauth.Transport{Config: config, Transport: http.DefaultTransport}
// ("Please ask the user if I can access this resource.")
url := config.AuthCodeURL("")
fmt.Println("Visit this URL to get a code, and enter the code.\n")
fmt.Println(url)
fmt.Print("Enter code: ")
var code string
fmt.Scanln(&code)
// Exchange the authorization code for an access token.
// ("Here's the code you gave the user, now give me a token!")
// TODO(bburns) : Put up a separate web end point to do the oauth dance, so a user can just go to a web page.
token, err := transport.Exchange(code)
if err != nil {
return err
}
// (The Exchange method will automatically cache the token.)
conf := &gceConfig{
Credential: gceCredential{
ClientId: clientId,
ClientSecret: clientSecret,
RefreshToken: token.RefreshToken,
},
Key: gceKey{Scope: scope},
ProjectId: projectId,
}
return conf.Write()
}
func newGCEOAuth2Config(clientId, clientSecret, scope string) *oauth.Config {
if clientId == "" {
clientId = gceDefaultClientID
}
if clientSecret == "" {
clientSecret = gceDefaultClientSecret
}
if scope == "" {
scope = gceDefaultScope
}
return &oauth.Config{
ClientId: clientId,
ClientSecret: clientSecret,
Scope: scope,
RedirectURL: "oob",
AuthURL: "https://accounts.google.com/o/oauth2/auth",
TokenURL: "https://accounts.google.com/o/oauth2/token",
AccessType: "offline",
}
}