instance.go (729 lines of code) (raw):
// Copyright 2017 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 daisy
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"path"
"regexp"
"strings"
"time"
daisyCompute "github.com/GoogleCloudPlatform/compute-daisy/compute"
computeBeta "google.golang.org/api/compute/v0.beta"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
)
const (
defaultAccessConfigType = "ONE_TO_ONE_NAT"
defaultDiskMode = diskModeRW
defaultDiskType = "pd-standard"
diskModeRO = "READ_ONLY"
diskModeRW = "READ_WRITE"
)
var (
instanceURLRgx = regexp.MustCompile(fmt.Sprintf(`^(projects/(?P<project>%[1]s)/)?zones/(?P<zone>%[2]s)/instances/(?P<instance>%[2]s)$`, projectRgxStr, rfc1035))
validDiskModes = []string{diskModeRO, diskModeRW}
)
func checkDiskMode(m string) bool {
parts := strings.Split(m, "/")
m = parts[len(parts)-1]
return strIn(m, validDiskModes)
}
// instanceExists should only be used during validation for existing GCE instances
// and should not be relied or populated for daisy created resources.
func (w *Workflow) instanceExists(project, zone, instance string) (bool, DError) {
return w.instanceCache.resourceExists(func(project, zone string, opts ...daisyCompute.ListCallOption) (interface{}, error) {
return w.ComputeClient.ListInstances(project, zone)
}, project, zone, instance)
}
// MarshalJSON is a workaround to prevent Instance from using compute.Instance's implementation.
func (i *Instance) MarshalJSON() ([]byte, error) {
return json.Marshal(*i)
}
// InstanceInterface represent abstract Instance across different API stages (Alpha, Beta, API)
type InstanceInterface interface {
getName() string
setName(name string)
getDescription() string
setDescription(description string)
getZone() string
setZone(zone string)
getMachineType() string
setMachineType(machineType string)
populateDisks(w *Workflow) DError
populateNetworks() DError
populateScopes() DError
initializeComputeMetadata()
appendComputeMetadata(key string, value *string)
validateNetworks(s *Step) (errs DError)
getComputeDisks() []*computeDisk
create(cc daisyCompute.Client) error
delete(cc daisyCompute.Client, deleteDisk bool) error
updateDisksAndNetworksBeforeCreate(w *Workflow)
getMetadata() map[string]string
setMetadata(md map[string]string)
getSourceMachineImage() string
setSourceMachineImage(machineImage string)
}
// InstanceBase is a base struct for GA/Beta instances.
// It holds the shared properties between the two.
type InstanceBase struct {
Resource
// OAuth2 scopes to give the instance. If left unset
// https://www.googleapis.com/auth/devstorage.read_only will be added.
Scopes []string `json:",omitempty"`
// StartupScript is the Sources path to a startup script to use in this step.
// This will be automatically mapped to the appropriate metadata key.
StartupScript string `json:",omitempty"`
// RetryWhenExternalIPDenied indicates whether to retry CreateInstances when
// it fails due to external IP denied by organization IP.
RetryWhenExternalIPDenied bool `json:",omitempty"`
// Should an existing instance of the same name be deleted, defaults to false
// which will fail validation.
OverWrite bool `json:",omitempty"`
// Serial port to log to GCS bucket, defaults to 1
SerialPortsToLog []int64 `json:",omitempty"`
}
// Instance is used to create a GCE instance using GA API.
// By default, output of serial port 1 will be streamed to the daisy logs directory.
type Instance struct {
InstanceBase
compute.Instance
// Additional metadata to set for the instance.
Metadata map[string]string `json:"metadata,omitempty"`
}
// InstanceBeta is used to create a GCE instance using Beta API.
// By default, output of serial port 1 will be streamed to the daisy logs directory.
type InstanceBeta struct {
InstanceBase
computeBeta.Instance
// Additional metadata to set for the instance.
Metadata map[string]string `json:"metadata,omitempty"`
}
func (i *Instance) getMachineType() string {
return i.MachineType
}
func (i *Instance) setMachineType(machineType string) {
i.MachineType = machineType
}
func (i *Instance) getDescription() string {
return i.Description
}
func (i *Instance) setDescription(description string) {
i.Description = description
}
func (i *Instance) getName() string {
return i.Name
}
func (i *Instance) setName(name string) {
i.Name = name
}
func (i *Instance) getZone() string {
return i.Zone
}
func (i *Instance) setZone(zone string) {
i.Zone = zone
}
func (i *Instance) initializeComputeMetadata() {
if i.Instance.Metadata == nil {
i.Instance.Metadata = &compute.Metadata{}
}
}
func (i *Instance) appendComputeMetadata(key string, value *string) {
i.Instance.Metadata.Items = append(i.Instance.Metadata.Items, &compute.MetadataItems{Key: key, Value: value})
}
func (i *Instance) create(cc daisyCompute.Client) error {
return cc.CreateInstance(i.Project, i.Zone, &i.Instance)
}
func (i *Instance) delete(cc daisyCompute.Client, deleteDisk bool) error {
return deleteInstance(deleteDisk, cc, i.Project, i.Zone, i.Name)
}
func (i *Instance) updateDisksAndNetworksBeforeCreate(w *Workflow) {
for _, d := range i.Disks {
if diskRes, ok := w.disks.get(d.Source); ok {
d.Source = diskRes.link
}
if d.InitializeParams != nil && d.InitializeParams.SourceImage != "" {
if image, ok := w.images.get(d.InitializeParams.SourceImage); ok {
d.InitializeParams.SourceImage = image.link
}
}
}
for _, n := range i.NetworkInterfaces {
if netRes, ok := w.networks.get(n.Network); ok {
n.Network = netRes.link
}
if subnetRes, ok := w.subnetworks.get(n.Subnetwork); ok {
n.Subnetwork = subnetRes.link
}
}
}
func (i *Instance) getMetadata() map[string]string {
return i.Metadata
}
func (i *Instance) setMetadata(md map[string]string) {
i.Metadata = md
}
func (i *Instance) getSourceMachineImage() string {
return i.Instance.SourceMachineImage
}
func (i *Instance) setSourceMachineImage(machineImage string) {
i.SourceMachineImage = machineImage
}
func (i *Instance) register(name string, s *Step, ir *instanceRegistry, errs DError) {
// Register disk attachments.
for _, d := range i.Disks {
diskName := d.Source
if d.InitializeParams != nil {
parts := NamedSubexp(diskTypeURLRgx, d.InitializeParams.DiskType)
if parts["disktype"] == "local-ssd" {
continue
}
diskName = d.InitializeParams.DiskName
}
errs = addErrs(errs, ir.w.disks.regAttach(d.DeviceName, diskName, name, d.Mode, s))
}
// Register network connections.
for _, n := range i.NetworkInterfaces {
nName := n.Network
errs = addErrs(errs, ir.w.networks.regConnect(nName, name, s))
}
}
func (i *InstanceBeta) getMachineType() string {
return i.MachineType
}
func (i *InstanceBeta) setMachineType(machineType string) {
i.MachineType = machineType
}
func (i *InstanceBeta) getDescription() string {
return i.Description
}
func (i *InstanceBeta) setDescription(description string) {
i.Description = description
}
func (i *InstanceBeta) getName() string {
return i.Name
}
func (i *InstanceBeta) setName(name string) {
i.Name = name
}
func (i *InstanceBeta) getZone() string {
return i.Zone
}
func (i *InstanceBeta) setZone(zone string) {
i.Zone = zone
}
func (i *InstanceBeta) appendComputeMetadata(key string, value *string) {
i.Instance.Metadata.Items = append(i.Instance.Metadata.Items, &computeBeta.MetadataItems{Key: key, Value: value})
}
func (i *InstanceBeta) initializeComputeMetadata() {
if i.Instance.Metadata == nil {
i.Instance.Metadata = &computeBeta.Metadata{}
}
}
func (i *InstanceBeta) create(cc daisyCompute.Client) error {
return cc.CreateInstanceBeta(i.Project, i.Zone, &i.Instance)
}
func (i *InstanceBeta) delete(cc daisyCompute.Client, deleteDisk bool) error {
return deleteInstance(deleteDisk, cc, i.Project, i.Zone, i.Name)
}
func (i *InstanceBeta) updateDisksAndNetworksBeforeCreate(w *Workflow) {
for _, d := range i.Disks {
if diskRes, ok := w.disks.get(d.Source); ok {
d.Source = diskRes.link
}
if d.InitializeParams != nil && d.InitializeParams.SourceImage != "" {
if image, ok := w.images.get(d.InitializeParams.SourceImage); ok {
d.InitializeParams.SourceImage = image.link
}
}
}
for _, n := range i.NetworkInterfaces {
if netRes, ok := w.networks.get(n.Network); ok {
n.Network = netRes.link
}
if subnetRes, ok := w.subnetworks.get(n.Subnetwork); ok {
n.Subnetwork = subnetRes.link
}
}
}
func (i *InstanceBeta) getMetadata() map[string]string {
return i.Metadata
}
func (i *InstanceBeta) setMetadata(md map[string]string) {
i.Metadata = md
}
func (i *InstanceBeta) getSourceMachineImage() string {
return i.Instance.SourceMachineImage
}
func (i *InstanceBeta) setSourceMachineImage(machineImage string) {
i.SourceMachineImage = machineImage
}
func (i *InstanceBeta) register(name string, s *Step, ir *instanceRegistry, errs DError) {
// Register disk attachments.
for _, d := range i.Disks {
diskName := d.Source
if d.InitializeParams != nil {
parts := NamedSubexp(diskTypeURLRgx, d.InitializeParams.DiskType)
if parts["disktype"] == "local-ssd" {
continue
}
diskName = d.InitializeParams.DiskName
}
errs = addErrs(errs, ir.w.disks.regAttach(d.DeviceName, diskName, name, d.Mode, s))
}
// Register network connections.
for _, n := range i.NetworkInterfaces {
nName := n.Network
errs = addErrs(errs, ir.w.networks.regConnect(nName, name, s))
}
}
func (ib *InstanceBase) populate(ctx context.Context, ii InstanceInterface, s *Step) DError {
name, zone, errs := ib.Resource.populateWithZone(ctx, s, ii.getName(), ii.getZone())
ii.setName(name)
ii.setZone(zone)
ii.setDescription(strOr(ii.getDescription(), fmt.Sprintf("Instance created by Daisy in workflow %q on behalf of %s.", s.w.Name, s.w.username)))
errs = addErrs(errs, ib.populateSerialPortsToLog())
errs = addErrs(errs, ii.populateDisks(s.w))
errs = addErrs(errs, ib.populateMachineType(ii))
errs = addErrs(errs, ib.populateMetadata(ii, s.w))
errs = addErrs(errs, ii.populateNetworks())
errs = addErrs(errs, ii.populateScopes())
ib.link = fmt.Sprintf("projects/%s/zones/%s/instances/%s", ib.Project, ii.getZone(), ii.getName())
if machineImageURLRgx.MatchString(ii.getSourceMachineImage()) {
ii.setSourceMachineImage(extendPartialURL(ii.getSourceMachineImage(), ib.Project))
}
return errs
}
func (i *Instance) populateDisks(w *Workflow) DError {
autonameIdx := 1
for di, d := range i.Disks {
d.Boot = di == 0
d.Mode = strOr(d.Mode, defaultDiskMode)
if diskURLRgx.MatchString(d.Source) {
d.Source = extendPartialURL(d.Source, i.Project)
}
p := d.InitializeParams
if p != nil {
// If name isn't set, set name to "instance-name", "instance-name-2", etc.
if p.DiskName == "" {
p.DiskName = i.Name
if autonameIdx > 1 {
p.DiskName = fmt.Sprintf("%s-%d", i.Name, autonameIdx)
}
autonameIdx++
}
if d.DeviceName == "" {
d.DeviceName = p.DiskName
}
// Extend SourceImage if short URL.
if imageURLRgx.MatchString(p.SourceImage) {
p.SourceImage = extendPartialURL(p.SourceImage, i.Project)
}
// Extend DiskType if short URL, or create extended URL.
p.DiskType = strOr(p.DiskType, defaultDiskType)
if diskTypeURLRgx.MatchString(p.DiskType) {
p.DiskType = extendPartialURL(p.DiskType, i.Project)
} else {
p.DiskType = fmt.Sprintf("projects/%s/zones/%s/diskTypes/%s", i.Project, i.Zone, p.DiskType)
}
parts := NamedSubexp(diskTypeURLRgx, p.DiskType)
if parts["disktype"] == "local-ssd" {
d.AutoDelete = true
d.Type = "SCRATCH"
p.DiskName = ""
}
} else if d.DeviceName == "" {
d.DeviceName = path.Base(d.Source)
}
}
return nil
}
func (i *InstanceBeta) populateDisks(w *Workflow) DError {
autonameIdx := 1
for di, d := range i.Disks {
d.Boot = di == 0
d.Mode = strOr(d.Mode, defaultDiskMode)
if diskURLRgx.MatchString(d.Source) {
d.Source = extendPartialURL(d.Source, i.Project)
}
p := d.InitializeParams
if p != nil {
// If name isn't set, set name to "instance-name", "instance-name-2", etc.
if p.DiskName == "" {
p.DiskName = i.Name
if autonameIdx > 1 {
p.DiskName = fmt.Sprintf("%s-%d", i.Name, autonameIdx)
}
autonameIdx++
}
if d.DeviceName == "" {
d.DeviceName = p.DiskName
}
// Extend SourceImage if short URL.
if imageURLRgx.MatchString(p.SourceImage) {
p.SourceImage = extendPartialURL(p.SourceImage, i.Project)
}
// Extend DiskType if short URL, or create extended URL.
p.DiskType = strOr(p.DiskType, defaultDiskType)
if diskTypeURLRgx.MatchString(p.DiskType) {
p.DiskType = extendPartialURL(p.DiskType, i.Project)
} else {
p.DiskType = fmt.Sprintf("projects/%s/zones/%s/diskTypes/%s", i.Project, i.Zone, p.DiskType)
}
parts := NamedSubexp(diskTypeURLRgx, p.DiskType)
if parts["disktype"] == "local-ssd" {
d.AutoDelete = true
d.Type = "SCRATCH"
p.DiskName = ""
}
} else if d.DeviceName == "" {
d.DeviceName = path.Base(d.Source)
}
}
return nil
}
func uniqueSerialPortsToLog(slice []int64) []int64 {
keys := make(map[int64]bool)
list := []int64{}
for _, entry := range slice {
if _, value := keys[entry]; !value {
keys[entry] = true
list = append(list, entry)
}
}
return list
}
func (ib *InstanceBase) populateSerialPortsToLog() DError {
if ib.SerialPortsToLog == nil {
ib.SerialPortsToLog = append(ib.SerialPortsToLog, 1)
} else {
ib.SerialPortsToLog = uniqueSerialPortsToLog(ib.SerialPortsToLog)
}
return nil
}
func (ib *InstanceBase) populateMachineType(ii InstanceInterface) DError {
// when creating instance from a machine image, don't set default machine type
if ii.getSourceMachineImage() != "" && ii.getMachineType() == "" {
return nil
}
ii.setMachineType(strOr(ii.getMachineType(), "n1-standard-1"))
if machineTypeURLRegex.MatchString(ii.getMachineType()) {
ii.setMachineType(extendPartialURL(ii.getMachineType(), ib.Project))
} else {
ii.setMachineType(fmt.Sprintf("projects/%s/zones/%s/machineTypes/%s", ib.Project, ii.getZone(), ii.getMachineType()))
}
return nil
}
func (ib *InstanceBase) populateMetadata(ii InstanceInterface, w *Workflow) DError {
if ii.getMetadata() == nil {
ii.setMetadata(map[string]string{})
}
ii.initializeComputeMetadata()
ii.getMetadata()["daisy-sources-path"] = "gs://" + path.Join(w.bucket, w.sourcesPath)
ii.getMetadata()["daisy-logs-path"] = "gs://" + path.Join(w.bucket, w.logsPath)
ii.getMetadata()["daisy-outs-path"] = "gs://" + path.Join(w.bucket, w.outsPath)
if ib.StartupScript != "" {
if !w.sourceExists(ib.StartupScript) {
return Errf("bad value for StartupScript, source not found: %s", ib.StartupScript)
}
ib.StartupScript = "gs://" + path.Join(w.bucket, w.sourcesPath, ib.StartupScript)
ii.getMetadata()["startup-script-url"] = ib.StartupScript
ii.getMetadata()["windows-startup-script-url"] = ib.StartupScript
}
for k, v := range ii.getMetadata() {
vCopy := v
ii.appendComputeMetadata(k, &vCopy)
}
return nil
}
func (i *Instance) populateNetworks() DError {
defaultAcs := []*compute.AccessConfig{{Type: defaultAccessConfigType}}
if i.NetworkInterfaces == nil {
i.NetworkInterfaces = []*compute.NetworkInterface{{}}
}
for _, n := range i.NetworkInterfaces {
if n.AccessConfigs == nil {
n.AccessConfigs = defaultAcs
}
// Only set deafult if no subnetwork or network set.
if n.Subnetwork == "" {
n.Network = strOr(n.Network, "global/networks/default")
}
if networkURLRegex.MatchString(n.Network) {
n.Network = extendPartialURL(n.Network, i.Project)
}
if subnetworkURLRegex.MatchString(n.Subnetwork) {
n.Subnetwork = extendPartialURL(n.Subnetwork, i.Project)
}
}
return nil
}
func (i *InstanceBeta) populateNetworks() DError {
defaultAcs := []*computeBeta.AccessConfig{{Type: defaultAccessConfigType}}
if i.NetworkInterfaces == nil {
i.NetworkInterfaces = []*computeBeta.NetworkInterface{{}}
}
for _, n := range i.NetworkInterfaces {
if n.AccessConfigs == nil {
n.AccessConfigs = defaultAcs
}
// Only set deafult if no subnetwork or network set.
if n.Subnetwork == "" {
n.Network = strOr(n.Network, "global/networks/default")
}
if networkURLRegex.MatchString(n.Network) {
n.Network = extendPartialURL(n.Network, i.Project)
}
if subnetworkURLRegex.MatchString(n.Subnetwork) {
n.Subnetwork = extendPartialURL(n.Subnetwork, i.Project)
}
}
return nil
}
func (i *Instance) populateScopes() DError {
if i.Scopes == nil {
i.Scopes = append(i.Scopes, "https://www.googleapis.com/auth/devstorage.read_only")
}
if i.ServiceAccounts == nil {
i.ServiceAccounts = []*compute.ServiceAccount{{Email: "default", Scopes: i.Scopes}}
}
return nil
}
func (i *InstanceBeta) populateScopes() DError {
if i.Scopes == nil {
i.Scopes = append(i.Scopes, "https://www.googleapis.com/auth/devstorage.read_only")
}
if i.ServiceAccounts == nil {
i.ServiceAccounts = []*computeBeta.ServiceAccount{{Email: "default", Scopes: i.Scopes}}
}
return nil
}
func (ib *InstanceBase) validate(ctx context.Context, ii InstanceInterface, s *Step) DError {
pre := fmt.Sprintf("cannot create instance %q", ib.daisyName)
errs := ib.Resource.validateWithZone(ctx, s, ii.getZone(), pre)
errs = addErrs(errs, ib.validateSerialPortsToLog())
errs = addErrs(errs, ib.validateDisks(ii, s))
errs = addErrs(errs, ib.validateMachineType(ii, s.w))
errs = addErrs(errs, ii.validateNetworks(s))
errs = addErrs(errs, ib.validateSourceMachineImage(ii, s))
// Register creation.
errs = addErrs(errs, s.w.instances.regCreate(ib.daisyName, &ib.Resource, ib.OverWrite, s))
return errs
}
func (ib *InstanceBase) validateSourceMachineImage(ii InstanceInterface, s *Step) DError {
// regUse needs the partal url of a non daisy resource.
lookup := ii.getSourceMachineImage()
if lookup == "" {
return nil
}
if _, err := s.w.machineImages.regUse(lookup, s); err != nil {
return newErr("failed to register use of machine image when creating an instance", err)
}
return nil
}
type computeDisk struct {
mode string
source string
hasInitializeParams bool
diskName string
sourceImage string
autoDelete bool
diskType string
}
func (i *Instance) getComputeDisks() []*computeDisk {
var computeDisks []*computeDisk
for _, d := range i.Disks {
computeDisk := computeDisk{mode: d.Mode, source: d.Source, hasInitializeParams: d.InitializeParams != nil, autoDelete: d.AutoDelete}
if computeDisk.hasInitializeParams {
computeDisk.diskName = d.InitializeParams.DiskName
computeDisk.sourceImage = d.InitializeParams.SourceImage
computeDisk.diskType = d.InitializeParams.DiskType
}
computeDisks = append(computeDisks, &computeDisk)
}
return computeDisks
}
func (i *InstanceBeta) getComputeDisks() []*computeDisk {
var computeDisks []*computeDisk
for _, d := range i.Disks {
computeDisk := computeDisk{mode: d.Mode, source: d.Source, hasInitializeParams: d.InitializeParams != nil, autoDelete: d.AutoDelete}
if computeDisk.hasInitializeParams {
computeDisk.diskName = d.InitializeParams.DiskName
computeDisk.sourceImage = d.InitializeParams.SourceImage
computeDisk.diskType = d.InitializeParams.DiskType
}
computeDisks = append(computeDisks, &computeDisk)
}
return computeDisks
}
func (ib *InstanceBase) validateSerialPortsToLog() (errs DError) {
for _, port := range ib.SerialPortsToLog {
if port < 0 || port > 4 {
errs = addErrs(errs, Errf("cannot create instance: SerialPortsToLog must be between 1-4, inclusive"))
}
}
return
}
func (ib *InstanceBase) validateDisks(ii InstanceInterface, s *Step) (errs DError) {
computeDisks := ii.getComputeDisks()
if len(computeDisks) == 0 && ii.getSourceMachineImage() == "" {
errs = addErrs(errs, Errf("cannot create instance: no disks nor source machine image provided"))
}
if len(computeDisks) > 0 && ii.getSourceMachineImage() != "" {
errs = addErrs(errs, Errf("cannot create instance: can't provide disks when SourceMachineImage provided"))
}
for _, d := range computeDisks {
if !checkDiskMode(d.mode) {
errs = addErrs(errs, Errf("cannot create instance: bad disk mode: %q", d.mode))
}
if d.source != "" && d.hasInitializeParams {
errs = addErrs(errs, Errf("cannot create instance: disk.source and disk.initializeParams are mutually exclusive"))
}
if d.hasInitializeParams {
errs = addErrs(errs, ib.validateDiskInitializeParams(d, ii, s))
} else {
errs = addErrs(errs, ib.validateDiskSource(d.source, ii, s))
}
}
return
}
func (ib *InstanceBase) validateDiskInitializeParams(d *computeDisk, ii InstanceInterface, s *Step) (errs DError) {
parts := NamedSubexp(diskTypeURLRgx, d.diskType)
if parts["project"] != ib.Project {
errs = addErrs(errs, Errf("cannot create instance in project %q with InitializeParams.DiskType in project %q", ib.Project, parts["project"]))
}
if parts["zone"] != ii.getZone() {
errs = addErrs(errs, Errf("cannot create instance in zone %q with InitializeParams.DiskType in zone %q", ii.getZone(), parts["zone"]))
}
if parts["disktype"] == "local-ssd" {
return
}
if _, err := s.w.images.regUse(d.sourceImage, s); err != nil {
errs = addErrs(errs, Errf("cannot create instance: can't use InitializeParams.SourceImage %q: %v", d.sourceImage, err))
}
if !rfc1035Rgx.MatchString(d.diskName) {
errs = addErrs(errs, Errf("cannot create instance: bad InitializeParams.DiskName: %q", d.diskName))
}
link := fmt.Sprintf("projects/%s/zones/%s/disks/%s", ib.Project, ii.getZone(), d.diskName)
// Set cleanup if not being autodeleted.
r := &Resource{RealName: d.diskName, link: link, NoCleanup: d.autoDelete}
errs = addErrs(errs, s.w.disks.regCreate(d.diskName, r, s, false))
return
}
func (ib *InstanceBase) validateDiskSource(diskSource string, ii InstanceInterface, s *Step) DError {
dr, errs := s.w.disks.regUse(diskSource, s)
if dr == nil {
// Return now, the rest of this function can't be run without dr.
return addErrs(errs, Errf("cannot create instance: disk %q not found in registry", diskSource))
}
// Ensure disk is in the same project and zone.
result := NamedSubexp(diskURLRgx, dr.link)
if result["project"] != ib.Project {
errs = addErrs(errs, Errf("cannot create instance in project %q with disk in project %q: %q", ib.Project, result["project"], diskSource))
}
if result["zone"] != ii.getZone() {
errs = addErrs(errs, Errf("cannot create instance in project %q with disk in zone %q: %q", ii.getZone(), result["zone"], diskSource))
}
return errs
}
func (ib *InstanceBase) validateMachineType(ii InstanceInterface, w *Workflow) (errs DError) {
if ii.getSourceMachineImage() != "" && ii.getMachineType() == "" {
return
}
if !machineTypeURLRegex.MatchString(ii.getMachineType()) {
errs = addErrs(errs, Errf("can't create instance: bad MachineType: %q", ii.getMachineType()))
return
}
result := NamedSubexp(machineTypeURLRegex, ii.getMachineType())
if result["project"] != ib.Project {
errs = addErrs(errs, Errf("cannot create instance in project %q with MachineType in project %q: %q", ib.Project, result["project"], ii.getMachineType()))
}
if result["zone"] != ii.getZone() {
errs = addErrs(errs, Errf("cannot create instance in zone %q with MachineType in zone %q: %q", ii.getZone(), result["zone"], ii.getMachineType()))
}
if exists, err := w.machineTypeExists(result["project"], result["zone"], result["machinetype"]); err != nil {
errs = addErrs(errs, Errf("cannot create instance, bad machineType lookup: %q, error: %v", result["machinetype"], err))
} else if !exists {
errs = addErrs(errs, Errf("cannot create instance, machineType does not exist: %q", result["machinetype"]))
}
return
}
func (i *Instance) validateNetworks(s *Step) (errs DError) {
for _, n := range i.NetworkInterfaces {
if n.Subnetwork != "" {
_, err := s.w.subnetworks.regUse(n.Subnetwork, s)
if err != nil {
errs = addErrs(errs, err)
}
}
if n.Network != "" {
_, err := s.w.networks.regUse(n.Network, s)
if err != nil {
errs = addErrs(errs, err)
continue
}
}
}
return
}
func (i *InstanceBeta) validateNetworks(s *Step) (errs DError) {
for _, n := range i.NetworkInterfaces {
if n.Subnetwork != "" {
_, err := s.w.subnetworks.regUse(n.Subnetwork, s)
if err != nil {
errs = addErrs(errs, err)
}
}
if n.Network != "" {
_, err := s.w.networks.regUse(n.Network, s)
if err != nil {
errs = addErrs(errs, err)
continue
}
}
}
return
}
type instanceRegistry struct {
baseResourceRegistry
}
func newInstanceRegistry(w *Workflow) *instanceRegistry {
ir := &instanceRegistry{baseResourceRegistry: baseResourceRegistry{w: w, typeName: "instance", urlRgx: instanceURLRgx}}
ir.baseResourceRegistry.deleteFn = ir.deleteFn
ir.baseResourceRegistry.startFn = ir.startFn
ir.baseResourceRegistry.stopFn = ir.stopFn
ir.init()
return ir
}
// SleepFn function is mocked on testing.
var SleepFn = time.Sleep
func (ir *instanceRegistry) deleteFn(res *Resource) DError {
m := NamedSubexp(instanceURLRgx, res.link)
for i := 1; i < 4; i++ {
if _, err := ir.w.ComputeClient.GetInstance(m["project"], m["zone"], m["instance"]); err != nil {
// Can't remove an instance that was not even yet created!
// However as the command was already submitted, wait.
SleepFn((time.Duration(rand.Intn(1000))*time.Millisecond + 1*time.Second) * time.Duration(i))
continue
}
}
// Proceed to instance deletion
err := ir.w.ComputeClient.DeleteInstance(m["project"], m["zone"], m["instance"])
if gErr, ok := err.(*googleapi.Error); ok && gErr.Code == http.StatusNotFound {
return typedErr(resourceDNEError, "failed to delete instance", err)
}
return newErr("failed to delete instance", err)
}
func (ir *instanceRegistry) startFn(res *Resource) DError {
m := NamedSubexp(instanceURLRgx, res.link)
err := ir.w.ComputeClient.StartInstance(m["project"], m["zone"], m["instance"])
if gErr, ok := err.(*googleapi.Error); ok && gErr.Code == http.StatusNotFound {
return typedErr(resourceDNEError, "failed to start instance", err)
}
return newErr("failed to start instance", err)
}
func (ir *instanceRegistry) stopFn(res *Resource) DError {
m := NamedSubexp(instanceURLRgx, res.link)
err := ir.w.ComputeClient.StopInstance(m["project"], m["zone"], m["instance"])
if gErr, ok := err.(*googleapi.Error); ok && gErr.Code == http.StatusNotFound {
return typedErr(resourceDNEError, "failed to stop instance", err)
}
return newErr("failed to stop instance", err)
}
func (ir *instanceRegistry) regCreate(name string, res *Resource, overWrite bool, s *Step) DError {
// Base creation logic.
errs := ir.baseResourceRegistry.regCreate(name, res, s, overWrite)
// Find the Instance responsible for this.
for _, i := range (*s.CreateInstances).Instances {
if &i.Resource == res {
i.register(name, s, ir, errs)
return errs
}
}
for _, i := range (*s.CreateInstances).InstancesBeta {
if &i.Resource == res {
i.register(name, s, ir, errs)
return errs
}
}
return errs
}
func (ir *instanceRegistry) regDelete(name string, s *Step) DError {
errs := ir.baseResourceRegistry.regDelete(name, s)
errs = addErrs(errs, ir.w.disks.regDetachAll(name, s))
return addErrs(errs, ir.w.networks.regDisconnectAll(name, s))
}
func deleteInstance(deleteDisk bool, cc daisyCompute.Client, project, zone, name string) error {
if !deleteDisk {
return cc.DeleteInstance(project, zone, name)
}
ci, err := cc.GetInstance(project, zone, name)
if err != nil {
return err
}
for _, cd := range ci.Disks {
if !cd.AutoDelete {
if err := cc.SetDiskAutoDelete(project, zone, name, true, cd.DeviceName); err != nil {
return err
}
}
}
return cc.DeleteInstance(project, zone, name)
}