image.go (366 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"
"net/http"
"path"
"regexp"
"strings"
computeAlpha "google.golang.org/api/compute/v0.alpha"
computeBeta "google.golang.org/api/compute/v0.beta"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
daisyCompute "github.com/GoogleCloudPlatform/compute-daisy/compute"
)
var (
imageURLRgx = regexp.MustCompile(fmt.Sprintf(`^(projects/(?P<project>%[1]s)/)?global/images\/((family/(?P<family>%[2]s))?|(?P<image>%[2]s))$`, projectRgxStr, rfc1035))
)
// imageExists should only be used during validation for existing GCE images
// and should not be relied or populated for daisy created resources.
func (w *Workflow) imageExists(project, family, image string) (bool, DError) {
if family != "" {
w.imageFamilyCache.mu.Lock()
defer w.imageFamilyCache.mu.Unlock()
if w.imageFamilyCache.exists == nil {
w.imageFamilyCache.exists = map[string]map[string]interface{}{}
}
if _, ok := w.imageFamilyCache.exists[project]; !ok {
w.imageFamilyCache.exists[project] = map[string]interface{}{}
}
if nameInResourceMap(image, w.imageFamilyCache.exists[project]) {
return true, nil
}
img, err := w.ComputeClient.GetImageFromFamily(project, family)
if err != nil {
if apiErr, ok := err.(*googleapi.Error); ok && apiErr.Code == http.StatusNotFound {
return false, nil
}
return false, typedErr(apiError, "failed to get image from family", err)
}
if img.Deprecated != nil {
if img.Deprecated.State == "OBSOLETE" || img.Deprecated.State == "DELETED" {
return true, typedErrf(imageObsoleteDeletedError, "image %q in state %q", img.Name, img.Deprecated.State)
}
}
w.imageFamilyCache.exists[project][img.Name] = img
return true, nil
}
if image == "" {
return false, Errf("must provide either family or name")
}
w.imageCache.mu.Lock()
defer w.imageCache.mu.Unlock()
var err error
if !w.disableCachingImages {
err = w.imageCache.loadCache(func(project string, opts ...daisyCompute.ListCallOption) (interface{}, error) {
return w.ComputeClient.ListImages(project)
}, project, image)
}
if w.disableCachingImages || err != nil {
ic, err := w.ComputeClient.GetImage(project, image)
if err != nil {
return false, typedErr(apiError, "error getting resource for project", err)
}
return true, errIfDeprecatedOrDeleted(ic, image)
}
for _, i := range w.imageCache.exists[project] {
if ic, ok := i.(*compute.Image); ok && image == ic.Name {
return true, errIfDeprecatedOrDeleted(ic, image)
}
}
return false, nil
}
func errIfDeprecatedOrDeleted(ic *compute.Image, image string) DError {
if ic.Deprecated != nil && (ic.Deprecated.State == "OBSOLETE" || ic.Deprecated.State == "DELETED") {
return typedErrf(imageObsoleteDeletedError, "image %q in state %q", image, ic.Deprecated.State)
}
return nil
}
// ImageInterface represent abstract Image across different API stages (Alpha, Beta, API)
type ImageInterface interface {
getName() string
setName(name string)
getDescription() string
setDescription(description string)
getSourceDisk() string
setSourceDisk(sourceDisk string)
getSourceImage() string
setSourceImage(sourceImage string)
hasRawDisk() bool
getRawDiskSource() string
setRawDiskSource(rawDiskSource string)
create(cc daisyCompute.Client) error
markCreatedInWorkflow()
delete(cc daisyCompute.Client) error
populateGuestOSFeatures()
}
// ImageBase is a base struct for GA/Beta/Alpha images. It holds the shared properties between them.
type ImageBase struct {
Resource
// Should an existing image of the same name be deleted, defaults to false
// which will fail validation.
OverWrite bool `json:",omitempty"`
//Ignores license validation if 403/forbidden returned
IgnoreLicenseValidationIfForbidden bool `json:",omitempty"`
}
// Image is used to create a GCE image using GA API.
// Supported sources are a GCE disk or a RAW image listed in Workflow.Sources.
type Image struct {
ImageBase
compute.Image
// GuestOsFeatures to set for the image.
GuestOsFeatures guestOsFeatures `json:"guestOsFeatures,omitempty"`
}
func (i *Image) getName() string {
return i.Name
}
func (i *Image) setName(name string) {
i.Name = name
}
func (i *Image) getDescription() string {
return i.Description
}
func (i *Image) setDescription(description string) {
i.Description = description
}
func (i *Image) getSourceDisk() string {
return i.SourceDisk
}
func (i *Image) setSourceDisk(sourceDisk string) {
i.SourceDisk = sourceDisk
}
func (i *Image) getSourceImage() string {
return i.SourceImage
}
func (i *Image) setSourceImage(sourceImage string) {
i.SourceImage = sourceImage
}
func (i *Image) hasRawDisk() bool {
return i.RawDisk != nil
}
func (i *Image) getRawDiskSource() string {
return i.RawDisk.Source
}
func (i *Image) setRawDiskSource(rawDiskSource string) {
i.RawDisk.Source = rawDiskSource
}
func (i *Image) create(cc daisyCompute.Client) error {
return cc.CreateImage(i.Project, &i.Image)
}
func (i *Image) markCreatedInWorkflow() {
i.createdInWorkflow = true
}
func (i *Image) delete(cc daisyCompute.Client) error {
return cc.DeleteImage(i.Project, i.Name)
}
func (i *Image) populateGuestOSFeatures() {
if i.GuestOsFeatures == nil {
return
}
for _, f := range i.GuestOsFeatures {
i.Image.GuestOsFeatures = append(i.Image.GuestOsFeatures, &compute.GuestOsFeature{Type: f})
}
}
// ImageBeta is used to create a GCE image using Beta API.
// Supported sources are a GCE disk or a RAW image listed in Workflow.Sources.
type ImageBeta struct {
ImageBase
computeBeta.Image
// GuestOsFeatures to set for the image.
GuestOsFeatures guestOsFeatures `json:"guestOsFeatures,omitempty"`
}
func (i *ImageBeta) getName() string {
return i.Name
}
func (i *ImageBeta) setName(name string) {
i.Name = name
}
func (i *ImageBeta) getDescription() string {
return i.Description
}
func (i *ImageBeta) setDescription(description string) {
i.Description = description
}
func (i *ImageBeta) getSourceDisk() string {
return i.SourceDisk
}
func (i *ImageBeta) setSourceDisk(sourceDisk string) {
i.SourceDisk = sourceDisk
}
func (i *ImageBeta) getSourceImage() string {
return i.SourceImage
}
func (i *ImageBeta) setSourceImage(sourceImage string) {
i.SourceImage = sourceImage
}
func (i *ImageBeta) hasRawDisk() bool {
return i.RawDisk != nil
}
func (i *ImageBeta) getRawDiskSource() string {
return i.RawDisk.Source
}
func (i *ImageBeta) setRawDiskSource(rawDiskSource string) {
i.RawDisk.Source = rawDiskSource
}
func (i *ImageBeta) create(cc daisyCompute.Client) error {
return cc.CreateImageBeta(i.Project, &i.Image)
}
func (i *ImageBeta) markCreatedInWorkflow() {
i.createdInWorkflow = true
}
func (i *ImageBeta) delete(cc daisyCompute.Client) error {
return cc.DeleteImage(i.Project, i.Name)
}
func (i *ImageBeta) populateGuestOSFeatures() {
if i.GuestOsFeatures == nil {
return
}
for _, f := range i.GuestOsFeatures {
i.Image.GuestOsFeatures = append(i.Image.GuestOsFeatures, &computeBeta.GuestOsFeature{Type: f})
}
}
// ImageAlpha is used to create a GCE image using Alpha API.
// Supported sources are a GCE disk or a RAW image listed in Workflow.Sources.
type ImageAlpha struct {
ImageBase
computeAlpha.Image
// GuestOsFeatures to set for the image.
GuestOsFeatures guestOsFeatures `json:"guestOsFeatures,omitempty"`
}
func (i *ImageAlpha) getName() string {
return i.Name
}
func (i *ImageAlpha) setName(name string) {
i.Name = name
}
func (i *ImageAlpha) getDescription() string {
return i.Description
}
func (i *ImageAlpha) setDescription(description string) {
i.Description = description
}
func (i *ImageAlpha) getSourceDisk() string {
return i.SourceDisk
}
func (i *ImageAlpha) setSourceDisk(sourceDisk string) {
i.SourceDisk = sourceDisk
}
func (i *ImageAlpha) getSourceImage() string {
return i.SourceImage
}
func (i *ImageAlpha) setSourceImage(sourceImage string) {
i.SourceImage = sourceImage
}
func (i *ImageAlpha) hasRawDisk() bool {
return i.RawDisk != nil
}
func (i *ImageAlpha) getRawDiskSource() string {
return i.RawDisk.Source
}
func (i *ImageAlpha) setRawDiskSource(rawDiskSource string) {
i.RawDisk.Source = rawDiskSource
}
func (i *ImageAlpha) create(cc daisyCompute.Client) error {
return cc.CreateImageAlpha(i.Project, &i.Image)
}
func (i *ImageAlpha) markCreatedInWorkflow() {
i.createdInWorkflow = true
}
func (i *ImageAlpha) delete(cc daisyCompute.Client) error {
return cc.DeleteImage(i.Project, i.Name)
}
func (i *ImageAlpha) populateGuestOSFeatures() {
if i.GuestOsFeatures == nil {
return
}
for _, f := range i.GuestOsFeatures {
i.Image.GuestOsFeatures = append(i.Image.GuestOsFeatures, &computeAlpha.GuestOsFeature{Type: f})
}
}
// MarshalJSON is a hacky workaround to prevent Image from using compute.Image's implementation.
func (i *Image) MarshalJSON() ([]byte, error) {
return json.Marshal(*i)
}
type guestOsFeatures []string
// UnmarshalJSON unmarshals GuestOsFeatures.
func (g *guestOsFeatures) UnmarshalJSON(b []byte) error {
// Support GCE API struct.
var cg []compute.GuestOsFeature
if err := json.Unmarshal(b, &cg); err == nil {
for _, f := range cg {
*g = append(*g, f.Type)
}
return nil
}
type dg guestOsFeatures
return json.Unmarshal(b, (*dg)(g))
}
func (ib *ImageBase) populate(ctx context.Context, ii ImageInterface, s *Step) DError {
name, errs := ib.Resource.populateWithGlobal(ctx, s, ii.getName())
ii.setName(name)
ii.setDescription(strOr(ii.getDescription(), fmt.Sprintf("Image created by Daisy in workflow %q on behalf of %s.", s.w.Name, s.w.username)))
if diskURLRgx.MatchString(ii.getSourceDisk()) {
ii.setSourceDisk(extendPartialURL(ii.getSourceDisk(), ib.Project))
}
if imageURLRgx.MatchString(ii.getSourceImage()) {
ii.setSourceImage(extendPartialURL(ii.getSourceImage(), ib.Project))
}
if ii.hasRawDisk() {
if s.w.sourceExists(ii.getRawDiskSource()) {
ii.setRawDiskSource(s.w.getSourceGCSAPIPath(ii.getRawDiskSource()))
} else if p, err := getGCSAPIPath(ii.getRawDiskSource()); err == nil {
ii.setRawDiskSource(p)
} else {
errs = addErrs(errs, Errf("bad value for RawDisk.Source: %q", ii.getRawDiskSource()))
}
}
ib.link = fmt.Sprintf("projects/%s/global/images/%s", ib.Project, ii.getName())
ii.populateGuestOSFeatures()
return errs
}
func (ib *ImageBase) validate(ctx context.Context, ii ImageInterface, licenses []string, s *Step) DError {
pre := fmt.Sprintf("cannot create image %q", ib.daisyName)
errs := ib.Resource.validate(ctx, s, pre)
if !xor(!xor(ii.getSourceDisk() == "", ii.getSourceImage() == ""), !ii.hasRawDisk()) {
errs = addErrs(errs, Errf("%s: must provide either SourceImage, SourceDisk or RawDisk, exclusively", pre))
}
// Source disk checking.
if ii.getSourceDisk() != "" {
if _, err := s.w.disks.regUse(ii.getSourceDisk(), s); err != nil {
errs = addErrs(errs, newErr("failed to get source disk", err))
}
}
// Source image checking.
if ii.getSourceImage() != "" {
_, err := s.w.images.regUse(ii.getSourceImage(), s)
errs = addErrs(errs, err)
}
// RawDisk.Source checking.
if ii.hasRawDisk() {
sBkt, sObj, err := splitGCSPath(ii.getRawDiskSource())
errs = addErrs(errs, err)
// Check if this image object is created by this workflow, otherwise check if object exists.
if !strIn(path.Join(sBkt, sObj), s.w.objects.created) && !strings.HasPrefix(sObj, s.w.outsPath) {
if _, err := s.w.StorageClient.Bucket(sBkt).Object(sObj).Attrs(ctx); err != nil {
errs = addErrs(errs, Errf("error reading object %s/%s: %v", sBkt, sObj, err))
}
}
}
// License checking.
for _, l := range licenses {
result := NamedSubexp(licenseURLRegex, l)
if exists, err := s.w.licenseExists(result["project"], result["license"]); err != nil {
if !(isGoogleAPIForbiddenError(err) && ib.IgnoreLicenseValidationIfForbidden) {
errs = addErrs(errs, Errf("%s: bad license lookup: %q, error: %v", pre, l, err))
}
} else if !exists {
errs = addErrs(errs, Errf("%s: license does not exist: %q", pre, l))
}
}
// Register image creation.
errs = addErrs(errs, s.w.images.regCreate(ib.daisyName, &ib.Resource, s, ib.OverWrite))
return errs
}
func isGoogleAPIForbiddenError(err DError) bool {
dErrConcrete, isDErrConcrete := err.(*dErrImpl)
if isDErrConcrete && len(dErrConcrete.errs) > 0 {
gAPIErr, isGAPIErr := dErrConcrete.errs[0].(*googleapi.Error)
if isGAPIErr && gAPIErr.Code == 403 {
return true
}
}
return false
}
type imageRegistry struct {
baseResourceRegistry
}
func newImageRegistry(w *Workflow) *imageRegistry {
ir := &imageRegistry{baseResourceRegistry: baseResourceRegistry{w: w, typeName: "image", urlRgx: imageURLRgx}}
ir.baseResourceRegistry.deleteFn = ir.deleteFn
ir.init()
return ir
}
func (ir *imageRegistry) deleteFn(res *Resource) DError {
m := NamedSubexp(imageURLRgx, res.link)
err := ir.w.ComputeClient.DeleteImage(m["project"], m["image"])
if gErr, ok := err.(*googleapi.Error); ok && gErr.Code == http.StatusNotFound {
return typedErr(resourceDNEError, "failed to delete image", err)
}
return newErr("failed to delete image", err)
}