resource.go (220 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"
"fmt"
"reflect"
"strings"
"sync"
"github.com/GoogleCloudPlatform/compute-daisy/compute"
)
// Resource is the base struct for Daisy representation structs for GCE resources.
// This base struct defines some common user-definable fields, as well as some Daisy bookkeeping fields.
type Resource struct {
// If this is unset Workflow.Project is used.
Project string `json:",omitempty"`
// Should this resource be cleaned up after the workflow?
NoCleanup bool `json:",omitempty"`
// If set Daisy will use this as the resource name instead of generating a name. Mutually exclusive with ExactName.
RealName string `json:",omitempty"`
// If set, Daisy will use the exact name as specified by the user instead of generating a name. Mutually exclusive with RealName.
ExactName bool `json:",omitempty"`
// The name of the disk as known to Daisy and the Daisy user.
daisyName string
link string
deleted bool
stoppedByWf bool
startedByWf bool
deleteMx *sync.Mutex
creator, deleter *Step
createdInWorkflow bool
users []*Step
}
func (r *Resource) populateWithGlobal(ctx context.Context, s *Step, name string) (string, DError) {
errs := r.populateHelper(ctx, s, name)
return r.RealName, errs
}
func (r *Resource) populateWithZone(ctx context.Context, s *Step, name, zone string) (string, string, DError) {
errs := r.populateHelper(ctx, s, name)
return r.RealName, strOr(zone, s.w.Zone), errs
}
func (r *Resource) populateWithRegion(ctx context.Context, s *Step, name, region string) (string, string, DError) {
errs := r.populateHelper(ctx, s, name)
return r.RealName, strOr(region, getRegionFromZone(s.w.Zone)), errs
}
func (r *Resource) populateHelper(ctx context.Context, s *Step, name string) DError {
var errs DError
if r.ExactName && r.RealName != "" {
errs = addErrs(errs, Errf("ExactName and RealName must be used mutually exclusively"))
} else if r.ExactName {
r.RealName = name
} else if r.RealName == "" {
r.RealName = s.w.genName(name)
}
r.daisyName = name
r.Project = strOr(r.Project, s.w.Project)
return errs
}
func (r *Resource) validate(ctx context.Context, s *Step, errPrefix string) DError {
var errs DError
if !checkName(r.RealName) {
return Errf("%s: bad name: %q", errPrefix, r.RealName)
}
if exists, err := projectExists(s.w.ComputeClient, r.Project); err != nil {
errs = addErrs(errs, Errf("%s: bad project lookup: %q, error: %v", errPrefix, r.Project, err))
} else if !exists {
errs = addErrs(errs, Errf("%s: project does not exist: %q", errPrefix, r.Project))
}
return errs
}
func (r *Resource) validateWithZone(ctx context.Context, s *Step, z, errPrefix string) DError {
errs := r.validate(ctx, s, errPrefix)
if z == "" {
errs = addErrs(errs, Errf("%s: no zone provided in step or workflow", errPrefix))
}
if exists, err := s.w.zoneExists(r.Project, z); err != nil {
errs = addErrs(errs, Errf("%s: bad zone lookup: %q, error: %v", errPrefix, z, err))
} else if !exists {
errs = addErrs(errs, Errf("%s: zone does not exist: %q", errPrefix, z))
}
return errs
}
func (r *Resource) validateWithRegion(ctx context.Context, s *Step, re, errPrefix string) DError {
errs := r.validate(ctx, s, errPrefix)
if re == "" {
errs = addErrs(errs, Errf("%s: no region provided in step or workflow", errPrefix))
}
if exists, err := s.w.regionExists(r.Project, re); err != nil {
errs = addErrs(errs, Errf("%s: bad region lookup: %q, error: %v", errPrefix, re, err))
} else if !exists {
errs = addErrs(errs, Errf("%s: region does not exist: %q", errPrefix, re))
}
return errs
}
func defaultDescription(resourceTypeName, wfName, user string) string {
return fmt.Sprintf("%s created by Daisy in workflow %q on behalf of %s.", resourceTypeName, wfName, user)
}
func extendPartialURL(url, project string) string {
if strings.HasPrefix(url, "projects") {
return url
}
return fmt.Sprintf("projects/%s/%s", project, url)
}
func (w *Workflow) resourceExists(url string) (bool, DError) {
if !strings.HasPrefix(url, "projects/") {
return false, Errf("partial GCE resource URL %q needs leading \"projects/PROJECT/\"", url)
}
switch {
case machineTypeURLRegex.MatchString(url):
result := NamedSubexp(machineTypeURLRegex, url)
return w.machineTypeExists(result["project"], result["zone"], result["machinetype"])
case instanceURLRgx.MatchString(url):
result := NamedSubexp(instanceURLRgx, url)
return w.instanceExists(result["project"], result["zone"], result["instance"])
case diskURLRgx.MatchString(url):
result := NamedSubexp(diskURLRgx, url)
return w.diskExists(result["project"], result["zone"], result["disk"])
case imageURLRgx.MatchString(url):
result := NamedSubexp(imageURLRgx, url)
return w.imageExists(result["project"], result["family"], result["image"])
case machineImageURLRgx.MatchString(url):
result := NamedSubexp(machineImageURLRgx, url)
return w.machineImageExists(result["project"], result["machineImage"])
case networkURLRegex.MatchString(url):
result := NamedSubexp(networkURLRegex, url)
return w.networkExists(result["project"], result["network"])
case subnetworkURLRegex.MatchString(url):
result := NamedSubexp(subnetworkURLRegex, url)
return w.subnetworkExists(result["project"], result["region"], result["subnetwork"])
case targetInstanceURLRegex.MatchString(url):
result := NamedSubexp(targetInstanceURLRegex, url)
return w.targetInstanceExists(result["project"], result["zone"], result["targetInstance"])
case forwardingRuleURLRegex.MatchString(url):
result := NamedSubexp(forwardingRuleURLRegex, url)
return w.forwardingRuleExists(result["project"], result["region"], result["forwardingRule"])
case firewallRuleURLRegex.MatchString(url):
result := NamedSubexp(firewallRuleURLRegex, url)
return w.firewallRuleExists(result["project"], result["firewallRule"])
case snapshotURLRgx.MatchString(url):
result := NamedSubexp(snapshotURLRgx, url)
return w.snapshotExists(result["project"], result["snapshot"])
}
return false, Errf("unknown resource type: %q", url)
}
func resourceNameHelper(name string, w *Workflow, exactName bool) string {
if !exactName {
name = w.genName(name)
}
return name
}
type twoDResourceCache struct {
exists map[string]map[string]map[string]interface{}
mu sync.Mutex
}
type oneDResourceCache struct {
exists map[string]map[string]interface{}
mu sync.Mutex
}
// resourceExists should only be used during validation for existing GCE
// resources and should not be relied or populated for daisy created resources.
func (c *twoDResourceCache) resourceExists(listResourceFunc func(project, regionOrZone string, opts ...compute.ListCallOption) (interface{}, error),
project, regionOrZone, resourceName string) (bool, DError) {
c.mu.Lock()
defer c.mu.Unlock()
err := c.loadCache(listResourceFunc, project, regionOrZone, resourceName)
if err != nil {
return false, err
}
return nameInResourceMap(resourceName, c.exists[project][regionOrZone]), nil
}
func (c *twoDResourceCache) loadCache(listResourceFunc func(project string, regionOrZone string, opts ...compute.ListCallOption) (interface{}, error),
project string, regionOrZone string, resourceName string) DError {
if resourceName == "" {
return Errf("must provide resource name")
}
if c.exists == nil {
c.exists = map[string]map[string]map[string]interface{}{}
}
if _, ok := c.exists[project]; !ok {
c.exists[project] = map[string]map[string]interface{}{}
}
if _, ok := c.exists[project][regionOrZone]; !ok {
ri, err := listResourceFunc(project, regionOrZone)
if err != nil {
return typedErr(apiError, "error listing resource for project", err)
}
c.exists[project][regionOrZone] = toMap(ri)
}
return nil
}
// resourceExists should only be used during validation for existing GCE
// resources and should not be relied or populated for daisy created resources.
func (c *oneDResourceCache) resourceExists(listResourceFunc func(project string, opts ...compute.ListCallOption) (interface{}, error),
project, resourceName string) (bool, DError) {
c.mu.Lock()
defer c.mu.Unlock()
err := c.loadCache(listResourceFunc, project, resourceName)
if err != nil {
return false, err
}
return nameInResourceMap(resourceName, c.exists[project]), nil
}
func (c *oneDResourceCache) loadCache(listResourceFunc func(project string, opts ...compute.ListCallOption) (interface{}, error), project string, resourceName string) DError {
if resourceName == "" {
return Errf("must provide resource name")
}
if c.exists == nil {
c.exists = map[string]map[string]interface{}{}
}
if _, ok := c.exists[project]; !ok {
ri, err := listResourceFunc(project)
if err != nil {
return typedErr(apiError, "error listing resource for project", err)
}
c.exists[project] = toMap(ri)
}
return nil
}
func toMap(slice interface{}) map[string]interface{} {
s := reflect.ValueOf(slice)
ret := make(map[string]interface{}, s.Len())
for i := 0; i < s.Len(); i++ {
r := s.Index(i).Interface()
v := reflect.ValueOf(r)
name := reflect.Indirect(v).FieldByName("Name").String()
ret[name] = r
}
return ret
}
func nameInResourceMap(name string, m map[string]interface{}) bool {
_, ok := m[name]
return ok
}