325 lines
9.5 KiB
Go
325 lines
9.5 KiB
Go
package googlecompute
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
|
)
|
|
|
|
// StepCreateInstance represents a Packer build step that creates GCE instances.
|
|
type StepCreateInstance struct {
|
|
Debug bool
|
|
}
|
|
|
|
func (c *Config) createInstanceMetadata(sourceImage *Image, sshPublicKey string) (map[string]string, map[string]string, error) {
|
|
|
|
instanceMetadataNoSSHKeys := make(map[string]string)
|
|
instanceMetadataSSHKeys := make(map[string]string)
|
|
|
|
sshMetaKey := "ssh-keys"
|
|
|
|
var err error
|
|
var errs *packersdk.MultiError
|
|
|
|
// Copy metadata from config.
|
|
for k, v := range c.Metadata {
|
|
if k == sshMetaKey {
|
|
instanceMetadataSSHKeys[k] = v
|
|
} else {
|
|
instanceMetadataNoSSHKeys[k] = v
|
|
}
|
|
}
|
|
|
|
// Merge any existing ssh keys with our public key, unless there is no
|
|
// supplied public key. This is possible if a private_key_file was
|
|
// specified.
|
|
if sshPublicKey != "" {
|
|
sshMetaKey := "ssh-keys"
|
|
sshPublicKey = strings.TrimSuffix(sshPublicKey, "\n")
|
|
sshKeys := fmt.Sprintf("%s:%s %s", c.Comm.SSHUsername, sshPublicKey, c.Comm.SSHUsername)
|
|
if confSSHKeys, exists := instanceMetadataSSHKeys[sshMetaKey]; exists {
|
|
sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSSHKeys)
|
|
}
|
|
instanceMetadataSSHKeys[sshMetaKey] = sshKeys
|
|
}
|
|
|
|
startupScript := instanceMetadataNoSSHKeys[StartupScriptKey]
|
|
if c.StartupScriptFile != "" {
|
|
var content []byte
|
|
content, err = ioutil.ReadFile(c.StartupScriptFile)
|
|
if err != nil {
|
|
return nil, instanceMetadataNoSSHKeys, err
|
|
}
|
|
startupScript = string(content)
|
|
}
|
|
instanceMetadataNoSSHKeys[StartupScriptKey] = startupScript
|
|
|
|
// Wrap any found startup script with our own startup script wrapper.
|
|
if startupScript != "" && c.WrapStartupScriptFile.True() {
|
|
instanceMetadataNoSSHKeys[StartupScriptKey] = StartupScriptLinux
|
|
instanceMetadataNoSSHKeys[StartupWrappedScriptKey] = startupScript
|
|
instanceMetadataNoSSHKeys[StartupScriptStatusKey] = StartupScriptStatusNotDone
|
|
}
|
|
|
|
if sourceImage.IsWindows() {
|
|
// Windows startup script support is not yet implemented so clear any script data and set status to done
|
|
instanceMetadataNoSSHKeys[StartupScriptKey] = StartupScriptWindows
|
|
instanceMetadataNoSSHKeys[StartupScriptStatusKey] = StartupScriptStatusDone
|
|
}
|
|
|
|
// If UseOSLogin is true, force `enable-oslogin` in metadata
|
|
// In the event that `enable-oslogin` is not enabled at project level
|
|
if c.UseOSLogin {
|
|
instanceMetadataNoSSHKeys[EnableOSLoginKey] = "TRUE"
|
|
}
|
|
|
|
for key, value := range c.MetadataFiles {
|
|
var content []byte
|
|
content, err = ioutil.ReadFile(value)
|
|
if err != nil {
|
|
errs = packersdk.MultiErrorAppend(errs, err)
|
|
}
|
|
instanceMetadataNoSSHKeys[key] = string(content)
|
|
}
|
|
|
|
if errs != nil && len(errs.Errors) > 0 {
|
|
return instanceMetadataNoSSHKeys, instanceMetadataSSHKeys, errs
|
|
}
|
|
return instanceMetadataNoSSHKeys, instanceMetadataSSHKeys, nil
|
|
}
|
|
|
|
func getImage(c *Config, d Driver) (*Image, error) {
|
|
name := c.SourceImageFamily
|
|
fromFamily := true
|
|
if c.SourceImage != "" {
|
|
name = c.SourceImage
|
|
fromFamily = false
|
|
}
|
|
if len(c.SourceImageProjectId) == 0 {
|
|
return d.GetImage(name, fromFamily)
|
|
} else {
|
|
return d.GetImageFromProjects(c.SourceImageProjectId, name, fromFamily)
|
|
}
|
|
}
|
|
|
|
// Run executes the Packer build step that creates a GCE instance.
|
|
func (s *StepCreateInstance) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
|
c := state.Get("config").(*Config)
|
|
d := state.Get("driver").(Driver)
|
|
|
|
ui := state.Get("ui").(packersdk.Ui)
|
|
|
|
sourceImage, err := getImage(c, d)
|
|
if err != nil {
|
|
err := fmt.Errorf("Error getting source image for instance creation: %s", err)
|
|
state.Put("error", err)
|
|
ui.Error(err.Error())
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
if c.EnableSecureBoot && !sourceImage.IsSecureBootCompatible() {
|
|
err := fmt.Errorf("Image: %s is not secure boot compatible. Please set 'enable_secure_boot' to false or choose another source image.", sourceImage.Name)
|
|
state.Put("error", err)
|
|
ui.Error(err.Error())
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
ui.Say(fmt.Sprintf("Using image: %s", sourceImage.Name))
|
|
|
|
if sourceImage.IsWindows() && c.Comm.Type == "winrm" && c.Comm.WinRMPassword == "" {
|
|
state.Put("create_windows_password", true)
|
|
}
|
|
|
|
ui.Say("Creating instance...")
|
|
name := c.InstanceName
|
|
|
|
var errCh <-chan error
|
|
var metadataNoSSHKeys map[string]string
|
|
var metadataSSHKeys map[string]string
|
|
metadataForInstance := make(map[string]string)
|
|
|
|
metadataNoSSHKeys, metadataSSHKeys, errs := c.createInstanceMetadata(sourceImage, string(c.Comm.SSHPublicKey))
|
|
if errs != nil {
|
|
state.Put("error", errs.Error())
|
|
ui.Error(errs.Error())
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
if c.WaitToAddSSHKeys > 0 {
|
|
log.Printf("[DEBUG] Adding metadata during instance creation, but not SSH keys...")
|
|
metadataForInstance = metadataNoSSHKeys
|
|
} else {
|
|
log.Printf("[DEBUG] Adding metadata during instance creation...")
|
|
|
|
// Union of both non-SSH key meta data and SSH key meta data
|
|
addmap(metadataForInstance, metadataSSHKeys)
|
|
addmap(metadataForInstance, metadataNoSSHKeys)
|
|
}
|
|
|
|
errCh, err = d.RunInstance(&InstanceConfig{
|
|
AcceleratorType: c.AcceleratorType,
|
|
AcceleratorCount: c.AcceleratorCount,
|
|
Address: c.Address,
|
|
Description: "New instance created by Packer",
|
|
DisableDefaultServiceAccount: c.DisableDefaultServiceAccount,
|
|
DiskSizeGb: c.DiskSizeGb,
|
|
DiskType: c.DiskType,
|
|
EnableSecureBoot: c.EnableSecureBoot,
|
|
EnableVtpm: c.EnableVtpm,
|
|
EnableIntegrityMonitoring: c.EnableIntegrityMonitoring,
|
|
Image: sourceImage,
|
|
Labels: c.Labels,
|
|
MachineType: c.MachineType,
|
|
Metadata: metadataForInstance,
|
|
MinCpuPlatform: c.MinCpuPlatform,
|
|
Name: name,
|
|
Network: c.Network,
|
|
NetworkProjectId: c.NetworkProjectId,
|
|
OmitExternalIP: c.OmitExternalIP,
|
|
OnHostMaintenance: c.OnHostMaintenance,
|
|
Preemptible: c.Preemptible,
|
|
Region: c.Region,
|
|
ServiceAccountEmail: c.ServiceAccountEmail,
|
|
Scopes: c.Scopes,
|
|
Subnetwork: c.Subnetwork,
|
|
Tags: c.Tags,
|
|
Zone: c.Zone,
|
|
})
|
|
|
|
if err == nil {
|
|
ui.Message("Waiting for creation operation to complete...")
|
|
select {
|
|
case err = <-errCh:
|
|
case <-time.After(c.StateTimeout):
|
|
err = errors.New("time out while waiting for instance to create")
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf("Error creating instance: %s", err)
|
|
state.Put("error", err)
|
|
ui.Error(err.Error())
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
ui.Message("Instance has been created!")
|
|
|
|
if s.Debug {
|
|
if name != "" {
|
|
ui.Message(fmt.Sprintf("Instance: %s started in %s", name, c.Zone))
|
|
}
|
|
}
|
|
|
|
// Things succeeded, store the name so we can remove it later
|
|
state.Put("instance_name", name)
|
|
// instance_id is the generic term used so that users can have access to the
|
|
// instance id inside of the provisioners, used in step_provision.
|
|
state.Put("instance_id", name)
|
|
|
|
if c.WaitToAddSSHKeys > 0 {
|
|
ui.Message(fmt.Sprintf("Waiting %s before adding SSH keys...",
|
|
c.WaitToAddSSHKeys.String()))
|
|
cancelled := s.waitForBoot(ctx, c.WaitToAddSSHKeys)
|
|
if cancelled {
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
log.Printf("[DEBUG] %s wait is over. Adding SSH keys to existing instance...",
|
|
c.WaitToAddSSHKeys.String())
|
|
err = d.AddToInstanceMetadata(c.Zone, name, metadataSSHKeys)
|
|
|
|
if err != nil {
|
|
err := fmt.Errorf("Error adding SSH keys to existing instance: %s", err)
|
|
state.Put("error", err)
|
|
ui.Error(err.Error())
|
|
return multistep.ActionHalt
|
|
}
|
|
}
|
|
|
|
return multistep.ActionContinue
|
|
}
|
|
|
|
func (s *StepCreateInstance) waitForBoot(ctx context.Context, waitLen time.Duration) bool {
|
|
// Use a select to determine if we get cancelled during the wait
|
|
select {
|
|
case <-ctx.Done():
|
|
return true
|
|
case <-time.After(waitLen):
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Cleanup destroys the GCE instance created during the image creation process.
|
|
func (s *StepCreateInstance) Cleanup(state multistep.StateBag) {
|
|
nameRaw, ok := state.GetOk("instance_name")
|
|
if !ok {
|
|
return
|
|
}
|
|
name := nameRaw.(string)
|
|
if name == "" {
|
|
return
|
|
}
|
|
|
|
config := state.Get("config").(*Config)
|
|
driver := state.Get("driver").(Driver)
|
|
ui := state.Get("ui").(packersdk.Ui)
|
|
|
|
ui.Say("Deleting instance...")
|
|
errCh, err := driver.DeleteInstance(config.Zone, name)
|
|
if err == nil {
|
|
select {
|
|
case err = <-errCh:
|
|
case <-time.After(config.StateTimeout):
|
|
err = errors.New("time out while waiting for instance to delete")
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
ui.Error(fmt.Sprintf(
|
|
"Error deleting instance. Please delete it manually.\n\n"+
|
|
"Name: %s\n"+
|
|
"Error: %s", name, err))
|
|
}
|
|
|
|
ui.Message("Instance has been deleted!")
|
|
state.Put("instance_name", "")
|
|
|
|
// Deleting the instance does not remove the boot disk. This cleanup removes
|
|
// the disk.
|
|
ui.Say("Deleting disk...")
|
|
errCh, err = driver.DeleteDisk(config.Zone, config.DiskName)
|
|
if err == nil {
|
|
select {
|
|
case err = <-errCh:
|
|
case <-time.After(config.StateTimeout):
|
|
err = errors.New("time out while waiting for disk to delete")
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
ui.Error(fmt.Sprintf(
|
|
"Error deleting disk. Please delete it manually.\n\n"+
|
|
"Name: %s\n"+
|
|
"Error: %s", config.InstanceName, err))
|
|
}
|
|
|
|
ui.Message("Disk has been deleted!")
|
|
|
|
return
|
|
}
|
|
|
|
func addmap(a map[string]string, b map[string]string) {
|
|
|
|
for k, v := range b {
|
|
a[k] = v
|
|
}
|
|
}
|