diff --git a/builder/googlecompute/account.go b/builder/googlecompute/account.go new file mode 100644 index 000000000..438181965 --- /dev/null +++ b/builder/googlecompute/account.go @@ -0,0 +1,35 @@ +package googlecompute + +import ( + "encoding/json" + "os" +) + +// accountFile represents the structure of the account file JSON file. +type accountFile struct { + PrivateKeyId string `json:"private_key_id"` + PrivateKey string `json:"private_key"` + ClientEmail string `json:"client_email"` + ClientId string `json:"client_id"` +} + +// clientSecretsFile represents the structure of the client secrets JSON file. +type clientSecretsFile struct { + Web struct { + AuthURI string `json:"auth_uri"` + ClientEmail string `json:"client_email"` + ClientId string `json:"client_id"` + TokenURI string `json:"token_uri"` + } +} + +func loadJSON(result interface{}, path string) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + dec := json.NewDecoder(f) + return dec.Decode(result) +} diff --git a/builder/googlecompute/builder.go b/builder/googlecompute/builder.go index 84c60d9a3..e5b3d171a 100644 --- a/builder/googlecompute/builder.go +++ b/builder/googlecompute/builder.go @@ -35,7 +35,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { // representing a GCE machine image. func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { driver, err := NewDriverGCE( - ui, b.config.ProjectId, b.config.clientSecrets, b.config.privateKeyBytes) + ui, b.config.ProjectId, &b.config.account, &b.config.clientSecrets) if err != nil { return nil, err } diff --git a/builder/googlecompute/client_secrets.go b/builder/googlecompute/client_secrets.go deleted file mode 100644 index cbe071514..000000000 --- a/builder/googlecompute/client_secrets.go +++ /dev/null @@ -1,32 +0,0 @@ -package googlecompute - -import ( - "encoding/json" - "io/ioutil" -) - -// clientSecrets represents the client secrets of a GCE service account. -type clientSecrets struct { - Web struct { - AuthURI string `json:"auth_uri"` - ClientEmail string `json:"client_email"` - ClientId string `json:"client_id"` - TokenURI string `json:"token_uri"` - } -} - -// loadClientSecrets loads the GCE client secrets file identified by path. -func loadClientSecrets(path string) (*clientSecrets, error) { - var cs *clientSecrets - secretBytes, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - - err = json.Unmarshal(secretBytes, &cs) - if err != nil { - return nil, err - } - - return cs, nil -} diff --git a/builder/googlecompute/client_secrets_test.go b/builder/googlecompute/client_secrets_test.go deleted file mode 100644 index 584aa0ce1..000000000 --- a/builder/googlecompute/client_secrets_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package googlecompute - -import ( - "io/ioutil" - "testing" -) - -func testClientSecretsFile(t *testing.T) string { - tf, err := ioutil.TempFile("", "packer") - if err != nil { - t.Fatalf("err: %s", err) - } - defer tf.Close() - - if _, err := tf.Write([]byte(testClientSecretsContent)); err != nil { - t.Fatalf("err: %s", err) - } - - return tf.Name() -} - -func TestLoadClientSecrets(t *testing.T) { - _, err := loadClientSecrets(testClientSecretsFile(t)) - if err != nil { - t.Fatalf("err: %s", err) - } -} - -// This is just some dummy data that doesn't actually work (it was revoked -// a long time ago). -const testClientSecretsContent = `{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_id":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}}` diff --git a/builder/googlecompute/config.go b/builder/googlecompute/config.go index 52df02f3a..5bed83b24 100644 --- a/builder/googlecompute/config.go +++ b/builder/googlecompute/config.go @@ -16,8 +16,11 @@ import ( type Config struct { common.PackerConfig `mapstructure:",squash"` + AccountFile string `mapstructure:"account_file"` + ClientSecretsFile string `mapstructure:"client_secrets_file"` + ProjectId string `mapstructure:"project_id"` + BucketName string `mapstructure:"bucket_name"` - ClientSecretsFile string `mapstructure:"client_secrets_file"` DiskSizeGb int64 `mapstructure:"disk_size"` ImageName string `mapstructure:"image_name"` ImageDescription string `mapstructure:"image_description"` @@ -25,9 +28,6 @@ type Config struct { MachineType string `mapstructure:"machine_type"` Metadata map[string]string `mapstructure:"metadata"` Network string `mapstructure:"network"` - Passphrase string `mapstructure:"passphrase"` - PrivateKeyFile string `mapstructure:"private_key_file"` - ProjectId string `mapstructure:"project_id"` SourceImage string `mapstructure:"source_image"` SourceImageProjectId string `mapstructure:"source_image_project_id"` SSHUsername string `mapstructure:"ssh_username"` @@ -37,7 +37,8 @@ type Config struct { Tags []string `mapstructure:"tags"` Zone string `mapstructure:"zone"` - clientSecrets *clientSecrets + account accountFile + clientSecrets clientSecretsFile instanceName string privateKeyBytes []byte sshTimeout time.Duration @@ -104,15 +105,15 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { // Process Templates templates := map[string]*string{ + "account_file": &c.AccountFile, + "client_secrets_file": &c.ClientSecretsFile, + "bucket_name": &c.BucketName, - "client_secrets_file": &c.ClientSecretsFile, "image_name": &c.ImageName, "image_description": &c.ImageDescription, "instance_name": &c.InstanceName, "machine_type": &c.MachineType, "network": &c.Network, - "passphrase": &c.Passphrase, - "private_key_file": &c.PrivateKeyFile, "project_id": &c.ProjectId, "source_image": &c.SourceImage, "source_image_project_id": &c.SourceImageProjectId, @@ -137,16 +138,16 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { errs, errors.New("a bucket_name must be specified")) } + if c.AccountFile == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("an account_file must be specified")) + } + if c.ClientSecretsFile == "" { errs = packer.MultiErrorAppend( errs, errors.New("a client_secrets_file must be specified")) } - if c.PrivateKeyFile == "" { - errs = packer.MultiErrorAppend( - errs, errors.New("a private_key_file must be specified")) - } - if c.ProjectId == "" { errs = packer.MultiErrorAppend( errs, errors.New("a project_id must be specified")) @@ -177,22 +178,17 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { } c.stateTimeout = stateTimeout - if c.ClientSecretsFile != "" { - // Load the client secrets file. - cs, err := loadClientSecrets(c.ClientSecretsFile) - if err != nil { + if c.AccountFile != "" { + if err := loadJSON(&c.account, c.AccountFile); err != nil { errs = packer.MultiErrorAppend( - errs, fmt.Errorf("Failed parsing client secrets file: %s", err)) + errs, fmt.Errorf("Failed parsing account file: %s", err)) } - c.clientSecrets = cs } - if c.PrivateKeyFile != "" { - // Load the private key. - c.privateKeyBytes, err = processPrivateKeyFile(c.PrivateKeyFile, c.Passphrase) - if err != nil { + if c.ClientSecretsFile != "" { + if err := loadJSON(&c.clientSecrets, c.ClientSecretsFile); err != nil { errs = packer.MultiErrorAppend( - errs, fmt.Errorf("Failed loading private key file: %s", err)) + errs, fmt.Errorf("Failed parsing client secrets file: %s", err)) } } diff --git a/builder/googlecompute/config_test.go b/builder/googlecompute/config_test.go index 6fa1425db..f99b9fece 100644 --- a/builder/googlecompute/config_test.go +++ b/builder/googlecompute/config_test.go @@ -1,14 +1,15 @@ package googlecompute import ( + "io/ioutil" "testing" ) func testConfig(t *testing.T) map[string]interface{} { return map[string]interface{}{ + "account_file": testAccountFile(t), "bucket_name": "foo", "client_secrets_file": testClientSecretsFile(t), - "private_key_file": testPrivateKeyFile(t), "project_id": "hashicorp", "source_image": "foo", "zone": "us-east-1a", @@ -84,16 +85,6 @@ func TestConfigPrepare(t *testing.T) { true, }, - { - "private_key_file", - nil, - true, - }, - { - "private_key_file", - testPrivateKeyFile(t), - false, - }, { "private_key_file", "/tmp/i/should/not/exist", @@ -174,3 +165,37 @@ func TestConfigPrepare(t *testing.T) { } } } + +func testAccountFile(t *testing.T) string { + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer tf.Close() + + if _, err := tf.Write([]byte(testAccountContent)); err != nil { + t.Fatalf("err: %s", err) + } + + return tf.Name() +} + +func testClientSecretsFile(t *testing.T) string { + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer tf.Close() + + if _, err := tf.Write([]byte(testClientSecretsContent)); err != nil { + t.Fatalf("err: %s", err) + } + + return tf.Name() +} + +// This is just some dummy data that doesn't actually work (it was revoked +// a long time ago). +const testAccountContent = `{}` + +const testClientSecretsContent = `{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_id":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}}` diff --git a/builder/googlecompute/driver_gce.go b/builder/googlecompute/driver_gce.go index 1439b7e3a..0936d36a0 100644 --- a/builder/googlecompute/driver_gce.go +++ b/builder/googlecompute/driver_gce.go @@ -23,22 +23,27 @@ type driverGCE struct { const DriverScopes string = "https://www.googleapis.com/auth/compute " + "https://www.googleapis.com/auth/devstorage.full_control" -func NewDriverGCE(ui packer.Ui, projectId string, c *clientSecrets, key []byte) (Driver, error) { - log.Printf("[INFO] Requesting token...") - log.Printf("[INFO] -- Email: %s", c.Web.ClientEmail) +func NewDriverGCE(ui packer.Ui, p string, a *accountFile, c *clientSecretsFile) (Driver, error) { + // Get the token for use in our requests + log.Printf("[INFO] Requesting Google token...") + log.Printf("[INFO] -- Email: %s", a.ClientEmail) log.Printf("[INFO] -- Scopes: %s", DriverScopes) - log.Printf("[INFO] -- Private Key Length: %d", len(key)) + log.Printf("[INFO] -- Private Key Length: %d", len(a.PrivateKey)) log.Printf("[INFO] -- Token URL: %s", c.Web.TokenURI) - jwtTok := jwt.NewToken(c.Web.ClientEmail, DriverScopes, key) + jwtTok := jwt.NewToken( + a.ClientEmail, + DriverScopes, + []byte(a.PrivateKey)) jwtTok.ClaimSet.Aud = c.Web.TokenURI token, err := jwtTok.Assert(new(http.Client)) if err != nil { - return nil, err + return nil, fmt.Errorf("Error retrieving auth token: %s", err) } + // Instantiate the transport to communicate to Google transport := &oauth.Transport{ Config: &oauth.Config{ - ClientId: c.Web.ClientId, + ClientId: a.ClientId, Scope: DriverScopes, TokenURL: c.Web.TokenURI, AuthURL: c.Web.AuthURI, @@ -46,14 +51,14 @@ func NewDriverGCE(ui packer.Ui, projectId string, c *clientSecrets, key []byte) Token: token, } - log.Printf("[INFO] Instantiating client...") + log.Printf("[INFO] Instantiating GCE client...") service, err := compute.New(transport.Client()) if err != nil { return nil, err } return &driverGCE{ - projectId: projectId, + projectId: p, service: service, ui: ui, }, nil diff --git a/website/source/docs/builders/googlecompute.markdown b/website/source/docs/builders/googlecompute.markdown index b8e304d61..94cbaa24b 100644 --- a/website/source/docs/builders/googlecompute.markdown +++ b/website/source/docs/builders/googlecompute.markdown @@ -13,38 +13,27 @@ for use with [Google Compute Engine](https://cloud.google.com/products/compute-e (GCE) based on existing images. Google Compute Engine doesn't allow the creation of images from scratch. -## Setting Up API Access +## Authentication -There is a small setup step required in order to obtain the credentials -that Packer needs to use Google Compute Engine. This needs to be done only -once if you intend to share the credentials. +Authenticating with Google Cloud services requires two separate JSON +files: one which we call the _account file_ and the _client secrets file_. -In order for Packer to talk to Google Compute Engine, it will need -a _client secrets_ JSON file and a _client private key_. Both of these are -obtained from the [Google Cloud Console](https://cloud.google.com/console). +Both of these files are downloaded directly from the +[Google Developers Console](https://console.developers.google.com). To make +the process more straightforwarded, it is documented here. -Follow the steps below: +1. Log into the [Google Developers Console](https://console.developers.google.com) + and select a project. -1. Log into the [Google Cloud Console](https://cloud.google.com/console) -2. Click on the project you want to use Packer with (or create one if you - don't have one yet). -3. Click "APIs & auth" in the left sidebar -4. Click "Credentials" in the left sidebar -5. Click "Create New Client ID" and choose "Service Account" -6. A private key will be downloaded for you. Note the password for the private key! This private key is your _client private key_. -7. After creating the account, click "Download JSON". This is your _client secrets JSON_ file. Make sure you didn't download the JSON from the "OAuth 2.0" section! This is a common mistake and will cause the builder to not work. +2. Under the "APIs & Auth" section, click "Credentials." -Finally, one last step, you'll have to convert the `p12` file you -got from Google into the PEM format. You can do this with OpenSSL, which -is installed standard on most Unixes: +3. Click the "Download JSON" button under the "Compute Engine and App Engine" + account in the OAuth section. The file should start with "client\_secrets". + This is your _client secrets file_. -``` -$ openssl pkcs12 -in -nocerts -passin pass:notasecret \ - -nodes -out private_key.pem -``` - -The client secrets JSON you downloaded along with the new "private\_key.pem" -file are the two files you need to configure Packer with to talk to GCE. +4. Create a new OAuth client ID and select "Service Account" as the type + of account. Once created, a JSON file should be downloaded. This is your + _account file_. ## Basic Example @@ -57,8 +46,8 @@ files obtained in the previous section. { "type": "googlecompute", "bucket_name": "my-project-packer-images", + "account_file": "account.json", "client_secrets_file": "client_secret.json", - "private_key_file": "XXXXXX-privatekey.p12", "project_id": "my-project", "source_image": "debian-7-wheezy-v20140718", "zone": "us-central1-a" @@ -72,6 +61,9 @@ each category, the available options are alphabetized and described. ### Required: +* `account_file` (string) - The JSON file containing your account credentials. + Instructions for how to retrieve these are above. + * `bucket_name` (string) - The Google Cloud Storage bucket to store the images that are created. The bucket must already exist in your project. @@ -113,9 +105,6 @@ each category, the available options are alphabetized and described. * `network` (string) - The Google Compute network to use for the launched instance. Defaults to `default`. -* `passphrase` (string) - The passphrase to use if the `private_key_file` - is encrypted. - * `ssh_port` (integer) - The SSH port. Defaults to 22. * `ssh_timeout` (string) - The time to wait for SSH to become available.