Repository: osbuild/osbuild-composer
Pull request #1339 (2021-04-01 11:52:02)

github: mention the guides in the pull request template
The template mentioned only "adequate" documentation which can be
interpreted in many ways. Mention the news/unreleased directory and the
guides explicitly and include links for convenience.


This pull request includes:

- [ ] adequate testing for the new functionality or fixed issue
- [ ] adequate documentation informing people about the change

<!--
Thanks for proposing a change to osbuild-composer!

Please don't remove the above check list. These are things that each pull
request must have before it is merged. It helps maintainers to not forget
anything.

If the reason for ticking any of the boxes is ambiguous, please add a short
note explaining why.

For user-visible changes, "adequate documentation" is an entry describing the
change for users in docs/news. Please refer to docs/news/README.md for details.

In addition, if this pull request fixes a downstream issue, please refer to
test/README.md and add these additional items:

- [ ] 1st commit of any `rhbz#` related PR contains bug reproducer; CI reports FAIL or
- [ ] PR contains automated tests for new functionality and
- [ ] QE has approved reproducer/new tests and
- [ ] Subsequent commits provide bug fixes without modifying the reproducer; CI reports PASS and
- [ ] QE approves this PR; RHBZ status is set to `MODIFIED + Verified=Tested`
-->



Patch:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index e9449839a..75d0c59c8 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -2,7 +2,9 @@
 This pull request includes:
 
 - [ ] adequate testing for the new functionality or fixed issue
-- [ ] adequate documentation informing people about the change
+- [ ] adequate documentation informing people about the change such as
+  - [ ] create a file in [news/unreleased](https://github.com/osbuild/osbuild-composer/tree/main/docs/news/unreleased) directory if this change should be mentioned in the release news
+  - [ ] submit a PR for the [guides](https://github.com/osbuild/guides) repository if this PR changed any behavior described there: https://www.osbuild.org/guides/
 
 <!--
 Thanks for proposing a change to osbuild-composer!
diff --git a/cmd/cloud-cleaner/main.go b/cmd/cloud-cleaner/main.go
index 6fc80e446..411171bd0 100644
--- a/cmd/cloud-cleaner/main.go
+++ b/cmd/cloud-cleaner/main.go
@@ -3,6 +3,7 @@
 package main
 
 import (
+	"context"
 	"crypto/sha256"
 	"fmt"
 	"log"
@@ -59,10 +60,12 @@ func cleanupGCP(testID string, wg *sync.WaitGroup) {
 		return
 	}
 
+	ctx := context.Background()
+
 	// Try to delete potentially running instance
 	log.Printf("[GCP] 🧹 Deleting VM instance %s in %s. "+
 		"This should fail if the test succedded.", GCPInstance, GCPZone)
-	err = g.ComputeInstanceDelete(GCPZone, GCPInstance)
+	err = g.ComputeInstanceDelete(ctx, GCPZone, GCPInstance)
 	if err != nil {
 		log.Printf("[GCP] Error: %v", err)
 	}
@@ -70,7 +73,7 @@ func cleanupGCP(testID string, wg *sync.WaitGroup) {
 	// Try to clean up storage of cache objects after image import job
 	log.Println("[GCP] 🧹 Cleaning up cache objects from storage after image " +
 		"import. This should fail if the test succedded.")
-	cacheObjects, errs := g.StorageImageImportCleanup(GCPImage)
+	cacheObjects, errs := g.StorageImageImportCleanup(ctx, GCPImage)
 	for _, err = range errs {
 		log.Printf("[GCP] Error: %v", err)
 	}
@@ -79,12 +82,12 @@ func cleanupGCP(testID string, wg *sync.WaitGroup) {
 	}
 
 	// Try to find the potentially uploaded Storage objects using custom metadata
-	objects, err := g.StorageListObjectsByMetadata(GCPBucket, map[string]string{gcp.MetadataKeyImageName: GCPImage})
+	objects, err := g.StorageListObjectsByMetadata(ctx, GCPBucket, map[string]string{gcp.MetadataKeyImageName: GCPImage})
 	if err != nil {
 		log.Printf("[GCP] Error: %v", err)
 	}
 	for _, obj := range objects {
-		if err = g.StorageObjectDelete(obj.Bucket, obj.Name); err != nil {
+		if err = g.StorageObjectDelete(ctx, obj.Bucket, obj.Name); err != nil {
 			log.Printf("[GCP] Error: %v", err)
 		}
 		log.Printf("[GCP] 🧹 Deleted object %s/%s related to build of image %s", obj.Bucket, obj.Name, GCPImage)
@@ -92,7 +95,7 @@ func cleanupGCP(testID string, wg *sync.WaitGroup) {
 
 	// Try to delete the imported image
 	log.Printf("[GCP] 🧹 Deleting image %s. This should fail if the test succedded.", GCPImage)
-	err = g.ComputeImageDelete(GCPImage)
+	err = g.ComputeImageDelete(ctx, GCPImage)
 	if err != nil {
 		log.Printf("[GCP] Error: %v", err)
 	}
diff --git a/cmd/osbuild-upload-gcp/main.go b/cmd/osbuild-upload-gcp/main.go
index 54431bb88..d276f496c 100644
--- a/cmd/osbuild-upload-gcp/main.go
+++ b/cmd/osbuild-upload-gcp/main.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"context"
 	"flag"
 	"fmt"
 	"io/ioutil"
@@ -39,7 +40,7 @@ func main() {
 	flag.StringVar(&objectName, "object", "", "Target Storage Object name")
 	flag.StringVar(&region, "region", "", "Target region for the uploaded image")
 	flag.StringVar(&osFamily, "os", "", "OS type used to determine which version of GCP guest tools to install")
-	flag.StringVar(&imageName, "image-name", "", "Image name after import to Compute Node")
+	flag.StringVar(&imageName, "image-name", "", "Image name after import to Compute Engine")
 	flag.StringVar(&imageFile, "image", "", "Image file to upload")
 	flag.Var(&shareWith, "share-with", "Accounts to share the image with. Can be set multiple times. Allowed values are 'user:{emailid}' / 'serviceAccount:{emailid}' / 'group:{emailid}' / 'domain:{domain}'.")
 	flag.BoolVar(&skipUpload, "skip-upload", false, "Use to skip Image Upload step")
@@ -60,39 +61,42 @@ func main() {
 		log.Fatalf("[GCP] Failed to create new GCP object: %v", err)
 	}
 
+	ctx := context.Background()
+
 	// Upload image to the Storage
 	if !skipUpload {
 		log.Printf("[GCP] 🚀 Uploading image to: %s/%s", bucketName, objectName)
-		_, err := g.StorageObjectUpload(imageFile, bucketName, objectName,
+		_, err := g.StorageObjectUpload(ctx, imageFile, bucketName, objectName,
 			map[string]string{gcp.MetadataKeyImageName: imageName})
 		if err != nil {
 			log.Fatalf("[GCP] Uploading image failed: %v", err)
 		}
 	}
 
-	// Import Image to Compute Node
+	// Import Image to Compute Engine
 	if !skipImport {
-		log.Printf("[GCP] 📥 Importing image into Compute Node as '%s'", imageName)
-		imageBuild, importErr := g.ComputeImageImport(bucketName, objectName, imageName, osFamily, region)
+		log.Printf("[GCP] 📥 Importing image into Compute Engine as '%s'", imageName)
+		imageBuild, importErr := g.ComputeImageImport(ctx, bucketName, objectName, imageName, osFamily, region)
 		if imageBuild != nil {
 			log.Printf("[GCP] 📜 Image import log URL: %s", imageBuild.LogUrl)
 			log.Printf("[GCP] 🎉 Image import finished with status: %s", imageBuild.Status)
+
+			// Cleanup all resources potentially left after the image import job
+			deleted, err := g.CloudbuildBuildCleanup(ctx, imageBuild.Id)
+			for _, d := range deleted {
+				log.Printf("[GCP] 🧹 Deleted resource after image import job: %s", d)
+			}
+			if err != nil {
+				log.Printf("[GCP] Encountered error during image import cleanup: %v", err)
+			}
 		}
 
 		// Cleanup storage before checking for errors
 		log.Printf("[GCP] 🧹 Deleting uploaded image file: %s/%s", bucketName, objectName)
-		if err = g.StorageObjectDelete(bucketName, objectName); err != nil {
+		if err = g.StorageObjectDelete(ctx, bucketName, objectName); err != nil {
 			log.Printf("[GCP] Encountered error while deleting object: %v", err)
 		}
 
-		deleted, errs := g.StorageImageImportCleanup(imageName)
-		for _, d := range deleted {
-			log.Printf("[GCP] 🧹 Deleted image import job file '%s'", d)
-		}
-		for _, e := range errs {
-			log.Printf("[GCP] Encountered error during image import cleanup: %v", e)
-		}
-
 		// check error from ComputeImageImport()
 		if importErr != nil {
 			log.Fatalf("[GCP] Importing image failed: %v", importErr)
@@ -103,7 +107,7 @@ func main() {
 	// Share the imported Image with specified accounts using IAM policy
 	if len(shareWith) > 0 {
 		log.Printf("[GCP] 🔗 Sharing the image with: %+v", shareWith)
-		err = g.ComputeImageShare(imageName, []string(shareWith))
+		err = g.ComputeImageShare(ctx, imageName, []string(shareWith))
 		if err != nil {
 			log.Fatalf("[GCP] Sharing image failed: %s", err)
 		}
diff --git a/cmd/osbuild-worker/jobimpl-osbuild.go b/cmd/osbuild-worker/jobimpl-osbuild.go
index 42ef0b6c3..17bb7522e 100644
--- a/cmd/osbuild-worker/jobimpl-osbuild.go
+++ b/cmd/osbuild-worker/jobimpl-osbuild.go
@@ -271,6 +271,8 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 			osbuildJobResult.Success = true
 			osbuildJobResult.UploadStatus = "success"
 		case *target.GCPTargetOptions:
+			ctx := context.Background()
+
 			g, err := gcp.New(impl.GCPCreds)
 			if err != nil {
 				appendTargetError(osbuildJobResult, err)
@@ -278,34 +280,35 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 			}
 
 			log.Printf("[GCP] 🚀 Uploading image to: %s/%s", options.Bucket, options.Object)
-			_, err = g.StorageObjectUpload(path.Join(outputDirectory, options.Filename),
+			_, err = g.StorageObjectUpload(ctx, path.Join(outputDirectory, options.Filename),
 				options.Bucket, options.Object, map[string]string{gcp.MetadataKeyImageName: args.Targets[0].ImageName})
 			if err != nil {
 				appendTargetError(osbuildJobResult, err)
 				return nil
 			}
 
-			log.Printf("[GCP] 📥 Importing image into Compute Node as '%s'", args.Targets[0].ImageName)
-			imageBuild, importErr := g.ComputeImageImport(options.Bucket, options.Object, args.Targets[0].ImageName, options.Os, options.Region)
+			log.Printf("[GCP] 📥 Importing image into Compute Engine as '%s'", args.Targets[0].ImageName)
+			imageBuild, importErr := g.ComputeImageImport(ctx, options.Bucket, options.Object, args.Targets[0].ImageName, options.Os, options.Region)
 			if imageBuild != nil {
 				log.Printf("[GCP] 📜 Image import log URL: %s", imageBuild.LogUrl)
 				log.Printf("[GCP] 🎉 Image import finished with status: %s", imageBuild.Status)
+
+				// Cleanup all resources potentially left after the image import job
+				deleted, err := g.CloudbuildBuildCleanup(ctx, imageBuild.Id)
+				for _, d := range deleted {
+					log.Printf("[GCP] 🧹 Deleted resource after image import job: %s", d)
+				}
+				if err != nil {
+					log.Printf("[GCP] Encountered error during image import cleanup: %v", err)
+				}
 			}
 
 			// Cleanup storage before checking for errors
 			log.Printf("[GCP] 🧹 Deleting uploaded image file: %s/%s", options.Bucket, options.Object)
-			if err = g.StorageObjectDelete(options.Bucket, options.Object); err != nil {
+			if err = g.StorageObjectDelete(ctx, options.Bucket, options.Object); err != nil {
 				log.Printf("[GCP] Encountered error while deleting object: %v", err)
 			}
 
-			deleted, errs := g.StorageImageImportCleanup(args.Targets[0].ImageName)
-			for _, d := range deleted {
-				log.Printf("[GCP] 🧹 Deleted image import job file '%s'", d)
-			}
-			for _, e := range errs {
-				log.Printf("[GCP] Encountered error during image import cleanup: %v", e)
-			}
-
 			// check error from ComputeImageImport()
 			if importErr != nil {
 				appendTargetError(osbuildJobResult, importErr)
@@ -315,7 +318,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 
 			if len(options.ShareWithAccounts) > 0 {
 				log.Printf("[GCP] 🔗 Sharing the image with: %+v", options.ShareWithAccounts)
-				err = g.ComputeImageShare(args.Targets[0].ImageName, options.ShareWithAccounts)
+				err = g.ComputeImageShare(ctx, args.Targets[0].ImageName, options.ShareWithAccounts)
 				if err != nil {
 					appendTargetError(osbuildJobResult, err)
 					return nil
diff --git a/internal/cloud/gcp/cloudbuild.go b/internal/cloud/gcp/cloudbuild.go
new file mode 100644
index 000000000..075a15b9e
--- /dev/null
+++ b/internal/cloud/gcp/cloudbuild.go
@@ -0,0 +1,226 @@
+package gcp
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"regexp"
+	"strings"
+	"time"
+
+	cloudbuild "cloud.google.com/go/cloudbuild/apiv1"
+	"cloud.google.com/go/storage"
+	"google.golang.org/api/iterator"
+	"google.golang.org/api/option"
+	cloudbuildpb "google.golang.org/genproto/googleapis/devtools/cloudbuild/v1"
+)
+
+// Structure with resources created by the Build job
+// Intended only for internal use
+type cloudbuildBuildResources struct {
+	zone             string
+	computeInstances []string
+	computeDisks     []string
+	storageCacheDir  struct {
+		bucket string
+		dir    string
+	}
+}
+
+// CloudbuildBuildLog fetches the log for the provided Build ID and returns it as a string
+//
+// Uses:
+//	- Storage API
+//	- Cloud Build API
+func (g *GCP) CloudbuildBuildLog(ctx context.Context, buildID string) (string, error) {
+	cloudbuildClient, err := cloudbuild.NewClient(ctx, option.WithCredentials(g.creds))
+	if err != nil {
+		return "", fmt.Errorf("failed to get Cloud Build client: %v", err)
+	}
+	defer cloudbuildClient.Close()
+
+	storageClient, err := storage.NewClient(ctx, option.WithCredentials(g.creds))
+	if err != nil {
+		return "", fmt.Errorf("failed to get Storage client: %v", err)
+	}
+	defer storageClient.Close()
+
+	getBuldReq := &cloudbuildpb.GetBuildRequest{
+		ProjectId: g.creds.ProjectID,
+		Id:        buildID,
+	}
+
+	imageBuild, err := cloudbuildClient.GetBuild(ctx, getBuldReq)
+	if err != nil {
+		return "", fmt.Errorf("failed to get the build info: %v", err)
+	}
+
+	// Determine the log file's Bucket and Object name
+	// Logs_bucket example: "gs://550072179371.cloudbuild-logs.googleusercontent.com"
+	// Logs file names will be of the format `${logs_bucket}/log-${build_id}.txt`
+	logBucket := imageBuild.LogsBucket
+	logBucket = strings.TrimPrefix(logBucket, "gs://")
+	// logBucket may contain directory in its name if set to a custom value
+	var logObjectDir string
+	if strings.Contains(logBucket, "/") {
+		ss := strings.SplitN(logBucket, "/", 2)
+		logBucket = ss[0]
+		logObjectDir = fmt.Sprintf("%s/", ss[1])
+	}
+	logObject := fmt.Sprintf("%slog-%s.txt", logObjectDir, buildID)
+
+	// Read the log
+	logBuilder := new(strings.Builder)
+	rd, err := storageClient.Bucket(logBucket).Object(logObject).NewReader(ctx)
+	if err != nil {
+		return "", fmt.Errorf("failed to create a new Reader for object '%s/%s': %v", logBucket, logObject, err)
+	}
+	_, err = io.Copy(logBuilder, rd)
+	if err != nil {
+		return "", fmt.Errorf("reading data from object '%s/%s' failed: %v", logBucket, logObject, err)
+	}
+
+	return logBuilder.String(), nil
+}
+
+// CloudbuildBuildCleanup parses the logs for the specified Build job and tries to clean up all resources
+// which were created as part of the job. It returns list of strings with all resources that were deleted
+// as a result of calling this method.
+//
+// Uses:
+//	- Storage API
+//	- Cloud Build API
+//	- Compute Engine API (indirectly)
+func (g *GCP) CloudbuildBuildCleanup(ctx context.Context, buildID string) ([]string, error) {
+	var deletedResources []string
+
+	storageClient, err := storage.NewClient(ctx, option.WithCredentials(g.creds))
+	if err != nil {
+		return deletedResources, fmt.Errorf("failed to get Storage client: %v", err)
+	}
+	defer storageClient.Close()
+
+	buildLog, err := g.CloudbuildBuildLog(ctx, buildID)
+	if err != nil {
+		return deletedResources, fmt.Errorf("failed to get log for build ID '%s': %v", buildID, err)
+	}
+
+	resources, err := cloudbuildResourcesFromBuildLog(buildLog)
+	if err != nil {
+		return deletedResources, fmt.Errorf("extracting created resources from build log failed: %v", err)
+	}
+
+	// Delete all Compute Engine instances
+	for _, instance := range resources.computeInstances {
+		err = g.ComputeInstanceDelete(ctx, resources.zone, instance)
+		if err == nil {
+			deletedResources = append(deletedResources, fmt.Sprintf("instance: %s (%s)", instance, resources.zone))
+		}
+	}
+
+	// Deleting instances in reality takes some time. Deleting a disk while it is still used by the instance, will fail.
+	// Iterate over the list of instances and wait until they are all deleted.
+	for _, instance := range resources.computeInstances {
+		for {
+			instanceInfo, err := g.ComputeInstanceGet(ctx, resources.zone, instance)
+			// Getting the instance information failed, it is ideleted.
+			if err != nil {
+				break
+			}
+			// Prevent an unlikely infinite loop of waiting on deletion of an instance which can't be deleted.
+			if instanceInfo.DeletionProtection {
+				break
+			}
+			time.Sleep(1 * time.Second)
+		}
+	}
+
+	// Delete all Compute Engine Disks
+	for _, disk := range resources.computeDisks {
+		err = g.ComputeDiskDelete(ctx, resources.zone, disk)
+		if err == nil {
+			deletedResources = append(deletedResources, fmt.Sprintf("disk: %s (%s)", disk, resources.zone))
+		}
+	}
+
+	// Delete all Storage cache files
+	bucket := storageClient.Bucket(resources.storageCacheDir.bucket)
+	objects := bucket.Objects(ctx, &storage.Query{Prefix: resources.storageCacheDir.dir})
+	for {
+		objAttrs, err := objects.Next()
+		if err == iterator.Done {
+			break
+		}
+		if err != nil {
+			// Do not return, just continue with the next object
+			continue
+		}
+
+		object := storageClient.Bucket(objAttrs.Bucket).Object(objAttrs.Name)
+		if err = object.Delete(ctx); err == nil {
+			deletedResources = append(deletedResources, fmt.Sprintf("storage object: %s/%s", objAttrs.Bucket, objAttrs.Name))
+		}
+	}
+
+	return deletedResources, nil
+}
+
+// cloudbuildResourcesFromBuildLog parses the provided Cloud Build log for any
+// resources that were created by the job as part of its work. The list of extracted
+// resources is returned as cloudbuildBuildResources struct instance
+func cloudbuildResourcesFromBuildLog(buildLog string) (*cloudbuildBuildResources, error) {
+	var resources cloudbuildBuildResources
+
+	// extract the used zone
+	// [inflate]: 2021-02-17T12:42:10Z Workflow Zone: europe-west1-b
+	zoneRe, err := regexp.Compile(`(?m)^.+Workflow Zone: (?P<zone>.+)$`)
+	if err != nil {
+		return &resources, err
+	}
+	zoneMatch := zoneRe.FindStringSubmatch(buildLog)
+	resources.zone = zoneMatch[1]
+
+	// extract Storage cache directory
+	// [inflate]: 2021-03-12T13:13:10Z Workflow GCSPath: gs://ascendant-braid-303513-daisy-bkt-us-central1/gce-image-import-2021-03-12T13:13:08Z-btgtd
+	cacheDirRe, err := regexp.Compile(`(?m)^.+Workflow GCSPath: gs://(?P<bucket>.+)/(?P<dir>.+)$`)
+	if err != nil {
+		return &resources, err
+	}
+	cacheDirMatch := cacheDirRe.FindStringSubmatch(buildLog)
+	resources.storageCacheDir.bucket = cacheDirMatch[1]
+	resources.storageCacheDir.dir = cacheDirMatch[2]
+
+	// extract Compute disks
+	// [inflate.setup-disks]: 2021-03-12T13:13:11Z CreateDisks: Creating disk "disk-importer-inflate-7366y".
+	// [inflate.setup-disks]: 2021-03-12T13:13:11Z CreateDisks: Creating disk "disk-inflate-scratch-7366y".
+	// [inflate.setup-disks]: 2021-03-12T13:13:11Z CreateDisks: Creating disk "disk-btgtd".
+	// [shadow-disk-checksum.create-disks]: 2021-03-12T17:29:54Z CreateDisks: Creating disk "disk-shadow-disk-checksum-shadow-disk-checksum-r3qxv".
+	disksRe, err := regexp.Compile(`(?m)^.+CreateDisks: Creating disk "(?P<disk>.+)".*$`)
+	if err != nil {
+		return &resources, err
+	}
+	disksMatches := disksRe.FindAllStringSubmatch(buildLog, -1)
+	for _, disksMatch := range disksMatches {
+		diskName := disksMatch[1]
+		if diskName != "" {
+			resources.computeDisks = append(resources.computeDisks, diskName)
+		}
+	}
+
+	// extract Compute instances
+	// [inflate.import-virtual-disk]: 2021-03-12T13:13:12Z CreateInstances: Creating instance "inst-importer-inflate-7366y".
+	// [shadow-disk-checksum.create-instance]: 2021-03-12T17:29:55Z CreateInstances: Creating instance "inst-shadow-disk-checksum-shadow-disk-checksum-r3qxv".
+	instancesRe, err := regexp.Compile(`(?m)^.+CreateInstances: Creating instance "(?P<instance>.+)".*$`)
+	if err != nil {
+		return &resources, err
+	}
+	instancesMatches := instancesRe.FindAllStringSubmatch(buildLog, -1)
+	for _, instanceMatch := range instancesMatches {
+		instanceName := instanceMatch[1]
+		if instanceName != "" {
+			resources.computeInstances = append(resources.computeInstances, instanceName)
+		}
+	}
+
+	return &resources, nil
+}
diff --git a/internal/cloud/gcp/compute.go b/internal/cloud/gcp/compute.go
index 80b5638d6..4158dd99f 100644
--- a/internal/cloud/gcp/compute.go
+++ b/internal/cloud/gcp/compute.go
@@ -14,14 +14,18 @@ import (
 )
 
 // ComputeImageImport imports a previously uploaded image by submitting a Cloud Build API
-// job. The job builds an image into Compute Node from an image uploaded to the
+// job. The job builds an image into Compute Engine from an image uploaded to the
 // storage.
 //
 // The Build job usually creates a number of cache files in the Storage.
 // This method does not do any cleanup, regardless if the image import succeeds or fails.
 //
-// To delete the Storage object used for image import, use StorageObjectDelete().
-// To cleanup cache files after the Build job, use StorageImageImportCleanup().
+// To delete the Storage object (image) used for the image import, use StorageObjectDelete().
+//
+// To delete all potentially left over resources after the Build job, use CloudbuildBuildCleanup().
+// This is especially important in case the image import is cancelled via the passed Context.
+// Cancelling the build leaves behind all resources that it created - instances and disks.
+// Therefore if you don't clean up the resources, they'll continue running and costing you money.
 //
 // bucket - Google storage bucket name with the uploaded image
 // object - Google storage object name of the uploaded image
@@ -43,8 +47,7 @@ import (
 //
 // Uses:
 //	- Cloud Build API
-func (g *GCP) ComputeImageImport(bucket, object, imageName, os, region string) (*cloudbuildpb.Build, error) {
-	ctx := context.Background()
+func (g *GCP) ComputeImageImport(ctx context.Context, bucket, object, imageName, os, region string) (*cloudbuildpb.Build, error) {
 	cloudbuildClient, err := cloudbuild.NewClient(ctx, option.WithCredentials(g.creds))
 	if err != nil {
 		return nil, fmt.Errorf("failed to get Cloud Build client: %v", err)
@@ -105,6 +108,24 @@ func (g *GCP) ComputeImageImport(bucket, object, imageName, os, region string) (
 
 	// Wait for the build to finish
 	for {
+		select {
+		case <-time.After(30 * time.Second):
+			// Just check the build status below
+		case <-ctx.Done():
+			// cancel the build
+			cancelBuildReq := &cloudbuildpb.CancelBuildRequest{
+				ProjectId: imageBuild.ProjectId,
+				Id:        imageBuild.Id,
+			}
+			// since the provided ctx has been canceled, create a new one to cancel the build
+			ctx = context.Background()
+			// Cancelling the build leaves behind all resources that it created (instances and disks)
+			imageBuild, err = cloudbuildClient.CancelBuild(ctx, cancelBuildReq)
+			if err != nil {
+				return imageBuild, fmt.Errorf("failed to cancel the image import build job: %v", err)
+			}
+		}
+
 		imageBuild, err = cloudbuildClient.GetBuild(ctx, getBuldReq)
 		if err != nil {
 			return imageBuild, fmt.Errorf("failed to get the build info: %v", err)
@@ -113,7 +134,6 @@ func (g *GCP) ComputeImageImport(bucket, object, imageName, os, region string) (
 		if imageBuild.Status != cloudbuildpb.Build_WORKING && imageBuild.Status != cloudbuildpb.Build_QUEUED {
 			break
 		}
-		time.Sleep(time.Second * 30)
 	}
 
 	if imageBuild.Status != cloudbuildpb.Build_SUCCESS {
@@ -148,9 +168,7 @@ func (g *GCP) ComputeImageURL(imageName string) string {
 //
 // Uses:
 //	- Compute Engine API
-func (g *GCP) ComputeImageShare(imageName string, shareWith []string) error {
-	ctx := context.Background()
-
+func (g *GCP) ComputeImageShare(ctx context.Context, imageName string, shareWith []string) error {
 	computeService, err := compute.NewService(ctx, option.WithCredentials(g.creds))
 	if err != nil {
 		return fmt.Errorf("failed to get Compute Engine client: %v", err)
@@ -193,7 +211,7 @@ func (g *GCP) ComputeImageShare(imageName string, shareWith []string) error {
 	// they can't use because of insufficient permissions.
 	//
 	// Even without the ability to view / list shared images, the user can still
-	// create a Compute Node instance using the image via API or 'gcloud' tool.
+	// create a Compute Engine instance using the image via API or 'gcloud' tool.
 	//
 	// Custom role to enable account to only list images in the project.
 	// Without this role, the account won't be able to list and see the image
@@ -205,14 +223,12 @@ func (g *GCP) ComputeImageShare(imageName string, shareWith []string) error {
 	return nil
 }
 
-// ComputeImageDelete deletes a Compute Node image with the given name. If the
+// ComputeImageDelete deletes a Compute Engine image with the given name. If the
 // image existed and was successfully deleted, no error is returned.
 //
 // Uses:
 //	- Compute Engine API
-func (g *GCP) ComputeImageDelete(image string) error {
-	ctx := context.Background()
-
+func (g *GCP) ComputeImageDelete(ctx context.Context, image string) error {
 	computeService, err := compute.NewService(ctx, option.WithCredentials(g.creds))
 	if err != nil {
 		return fmt.Errorf("failed to get Compute Engine client: %v", err)
@@ -223,15 +239,13 @@ func (g *GCP) ComputeImageDelete(image string) error {
 	return err
 }
 
-// ComputeInstanceDelete deletes a Compute Node instance with the given name and
+// ComputeInstanceDelete deletes a Compute Engine instance with the given name and
 // running in the given zone. If the instance existed and was successfully deleted,
 // no error is returned.
 //
 // Uses:
 //	- Compute Engine API
-func (g *GCP) ComputeInstanceDelete(zone, instance string) error {
-	ctx := context.Background()
-
+func (g *GCP) ComputeInstanceDelete(ctx context.Context, zone, instance string) error {
 	computeService, err := compute.NewService(ctx, option.WithCredentials(g.creds))
 	if err != nil {
 		return fmt.Errorf("failed to get Compute Engine client: %v", err)
@@ -241,3 +255,36 @@ func (g *GCP) ComputeInstanceDelete(zone, instance string) error {
 
 	return err
 }
+
+// ComputeInstanceGet fetches a Compute Engine instance information. If fetching the information
+// was successful, it is returned to the caller, otherwise <nil> is returned with a proper error.
+//
+// Uses:
+//	- Compute Engine API
+func (g *GCP) ComputeInstanceGet(ctx context.Context, zone, instance string) (*compute.Instance, error) {
+	computeService, err := compute.NewService(ctx, option.WithCredentials(g.creds))
+	if err != nil {
+		return nil, fmt.Errorf("failed to get Compute Engine client: %v", err)
+	}
+
+	resp, err := computeService.Instances.Get(g.creds.ProjectID, zone, instance).Context(ctx).Do()
+
+	return resp, err
+}
+
+// ComputeDiskDelete deletes a Compute Engine disk with the given name and
+// running in the given zone. If the disk existed and was successfully deleted,
+// no error is returned.
+//
+// Uses:
+//	- Compute Engine API
+func (g *GCP) ComputeDiskDelete(ctx context.Context, zone, disk string) error {
+	computeService, err := compute.NewService(ctx, option.WithCredentials(g.creds))
+	if err != nil {
+		return fmt.Errorf("failed to get Compute Engine client: %v", err)
+	}
+
+	_, err = computeService.Disks.Delete(g.creds.ProjectID, zone, disk).Context(ctx).Do()
+
+	return err
+}
diff --git a/internal/cloud/gcp/storage.go b/internal/cloud/gcp/storage.go
index b5aa63bb3..6b3d548c7 100644
--- a/internal/cloud/gcp/storage.go
+++ b/internal/cloud/gcp/storage.go
@@ -31,9 +31,7 @@ const (
 //
 // Uses:
 //	- Storage API
-func (g *GCP) StorageObjectUpload(filename, bucket, object string, metadata map[string]string) (*storage.ObjectAttrs, error) {
-	ctx := context.Background()
-
+func (g *GCP) StorageObjectUpload(ctx context.Context, filename, bucket, object string, metadata map[string]string) (*storage.ObjectAttrs, error) {
 	storageClient, err := storage.NewClient(ctx, option.WithCredentials(g.creds))
 	if err != nil {
 		return nil, fmt.Errorf("failed to get Storage client: %v", err)
@@ -85,9 +83,7 @@ func (g *GCP) StorageObjectUpload(filename, bucket, object string, metadata map[
 //
 // Uses:
 //	- Storage API
-func (g *GCP) StorageObjectDelete(bucket, object string) error {
-	ctx := context.Background()
-
+func (g *GCP) StorageObjectDelete(ctx context.Context, bucket, object string) error {
 	storageClient, err := storage.NewClient(ctx, option.WithCredentials(g.creds))
 	if err != nil {
 		return fmt.Errorf("failed to get Storage client: %v", err)
@@ -114,12 +110,10 @@ func (g *GCP) StorageObjectDelete(bucket, object string) error {
 // Uses:
 //	- Compute Engine API
 //	- Storage API
-func (g *GCP) StorageImageImportCleanup(imageName string) ([]string, []error) {
+func (g *GCP) StorageImageImportCleanup(ctx context.Context, imageName string) ([]string, []error) {
 	var deletedObjects []string
 	var errors []error
 
-	ctx := context.Background()
-
 	storageClient, err := storage.NewClient(ctx, option.WithCredentials(g.creds))
 	if err != nil {
 		errors = append(errors, fmt.Errorf("failed to get Storage client: %v", err))
@@ -205,9 +199,8 @@ func (g *GCP) StorageImageImportCleanup(imageName string) ([]string, []error) {
 //
 // Uses:
 //	- Storage API
-func (g *GCP) StorageListObjectsByMetadata(bucket string, metadata map[string]string) ([]*storage.ObjectAttrs, error) {
+func (g *GCP) StorageListObjectsByMetadata(ctx context.Context, bucket string, metadata map[string]string) ([]*storage.ObjectAttrs, error) {
 	var matchedObjectAttr []*storage.ObjectAttrs
-	ctx := context.Background()
 
 	storageClient, err := storage.NewClient(ctx, option.WithCredentials(g.creds))
 	if err != nil {
diff --git a/internal/cloudapi/openapi.gen.go b/internal/cloudapi/openapi.gen.go
index 80ef889d8..484acb143 100644
--- a/internal/cloudapi/openapi.gen.go
+++ b/internal/cloudapi/openapi.gen.go
@@ -107,7 +107,7 @@ type GCPUploadRequestOptions struct {
 	// Name of an existing STANDARD Storage class Bucket.
 	Bucket string `json:"bucket"`
 
-	// The name to use for the imported and shared Compute Node image.
+	// The name to use for the imported and shared Compute Engine image.
 	// The image name must be unique within the GCP project, which is used
 	// for the OS image upload and import. If not specified a random
 	// 'composer-api-<uuid>' string is used as the image name.
@@ -119,7 +119,7 @@ type GCPUploadRequestOptions struct {
 	// (source Storage Bucket location) is chosen automatically.
 	Region *string `json:"region,omitempty"`
 
-	// List of valid Google accounts to share the imported Compute Node image with.
+	// List of valid Google accounts to share the imported Compute Engine image with.
 	// Each string must contain a specifier of the account type. Valid formats are:
 	//   - 'user:{emailid}': An email address that represents a specific
 	//     Google account. For example, 'alice@example.com'.
@@ -129,7 +129,7 @@ type GCPUploadRequestOptions struct {
 	//     For example, 'admins@example.com'.
 	//   - 'domain:{domain}': The G Suite domain (primary) that represents all
 	//     the users of that domain. For example, 'google.com' or 'example.com'.
-	// If not specified, the imported Compute Node image is not shared with any
+	// If not specified, the imported Compute Engine image is not shared with any
 	// account.
 	ShareWithAccounts *[]string `json:"share_with_accounts,omitempty"`
 }
@@ -868,52 +868,52 @@ func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler {
 // Base64 encoded, gzipped, json marshaled Swagger object
 var swaggerSpec = []string{
 
-	"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",
+	"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",
 }
 
 // GetSwagger returns the Swagger specification corresponding to the generated code
diff --git a/internal/cloudapi/openapi.yml b/internal/cloudapi/openapi.yml
index 5a230753f..ba10faf25 100644
--- a/internal/cloudapi/openapi.yml
+++ b/internal/cloudapi/openapi.yml
@@ -302,7 +302,7 @@ components:
           type: string
           example: 'my-image'
           description: |
-            The name to use for the imported and shared Compute Node image.
+            The name to use for the imported and shared Compute Engine image.
             The image name must be unique within the GCP project, which is used
             for the OS image upload and import. If not specified a random
             'composer-api-<uuid>' string is used as the image name.
@@ -315,7 +315,7 @@ components:
             'domain:example.com'
             ]
           description: |
-            List of valid Google accounts to share the imported Compute Node image with.
+            List of valid Google accounts to share the imported Compute Engine image with.
             Each string must contain a specifier of the account type. Valid formats are:
               - 'user:{emailid}': An email address that represents a specific
                 Google account. For example, 'alice@example.com'.
@@ -325,7 +325,7 @@ components:
                 For example, 'admins@example.com'.
               - 'domain:{domain}': The G Suite domain (primary) that represents all
                 the users of that domain. For example, 'google.com' or 'example.com'.
-            If not specified, the imported Compute Node image is not shared with any
+            If not specified, the imported Compute Engine image is not shared with any
             account.
           items:
             type: string
diff --git a/schutzbot/Jenkinsfile b/schutzbot/Jenkinsfile
index 5c681bbc6..ae67fb25b 100644
--- a/schutzbot/Jenkinsfile
+++ b/schutzbot/Jenkinsfile
@@ -3,9 +3,12 @@ def cron_string = BRANCH_NAME == "main" ? "@daily" : ""
 pipeline {
     agent none
 
-    triggers {
-        cron(cron_string)
-    }
+// Don't run the nightly test on main. Main currently doesn't match the
+// osbuild-composer version shipped in any RHEL versions.
+// We should resume this when we start rebasing again.
+//    triggers {
+//        cron(cron_string)
+//    }
 
     environment {
         AWS_REGION = "us-east-2"
@@ -591,6 +594,13 @@ pipeline {
                     }
                 }
                 stage('EL9.0 Integration') {
+                    when {
+                        beforeAgent true
+                        expression {
+                            return env.BUILD_CAUSE != 'cron';
+                        }
+                    }
+
                     agent { label "rhel84cloudbase && x86_64 && psi" }
                     environment {
                         TEST_TYPE = "integration"


Test patch:
diff --git a/internal/cloud/gcp/cloudbuild_test.go b/internal/cloud/gcp/cloudbuild_test.go
new file mode 100644
index 000000000..6cb842708
--- /dev/null
+++ b/internal/cloud/gcp/cloudbuild_test.go
@@ -0,0 +1,425 @@
+package gcp
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestCloudbuildResourcesFromBuildLog(t *testing.T) {
+	testCases := []struct {
+		buildLog  string
+		resources cloudbuildBuildResources
+	}{
+		{
+			buildLog: `2021/03/15 18:07:56 starting build "dba8bd1a-79b7-4060-a99d-c334760cba18"
+
+FETCHSOURCE
+BUILD
+Pulling image: gcr.io/compute-image-tools/gce_vm_image_import:release
+release: Pulling from compute-image-tools/gce_vm_image_import
+0b8bbb5f50a4: Pulling fs layer
+7efaa022ad36: Pulling fs layer
+e5303db5f8f9: Pulling fs layer
+688d304ec274: Pulling fs layer
+e969b3a22ab3: Pulling fs layer
+cd7b8272632b: Pulling fs layer
+2175d0ddd745: Pulling fs layer
+69fbd73b475e: Pulling fs layer
+7a5922a992b2: Pulling fs layer
+688d304ec274: Waiting
+e969b3a22ab3: Waiting
+cd7b8272632b: Waiting
+2175d0ddd745: Waiting
+69fbd73b475e: Waiting
+7a5922a992b2: Waiting
+e5303db5f8f9: Verifying Checksum
+e5303db5f8f9: Download complete
+688d304ec274: Verifying Checksum
+688d304ec274: Download complete
+e969b3a22ab3: Verifying Checksum
+e969b3a22ab3: Download complete
+0b8bbb5f50a4: Verifying Checksum
+0b8bbb5f50a4: Download complete
+cd7b8272632b: Verifying Checksum
+cd7b8272632b: Download complete
+69fbd73b475e: Verifying Checksum
+69fbd73b475e: Download complete
+7efaa022ad36: Verifying Checksum
+7efaa022ad36: Download complete
+7a5922a992b2: Verifying Checksum
+7a5922a992b2: Download complete
+2175d0ddd745: Verifying Checksum
+2175d0ddd745: Download complete
+0b8bbb5f50a4: Pull complete
+7efaa022ad36: Pull complete
+e5303db5f8f9: Pull complete
+688d304ec274: Pull complete
+e969b3a22ab3: Pull complete
+cd7b8272632b: Pull complete
+2175d0ddd745: Pull complete
+69fbd73b475e: Pull complete
+7a5922a992b2: Pull complete
+Digest: sha256:d39e2c0e6a7113d989d292536e9d14e927de838cb21a24c61eb7d44fef1fa51d
+Status: Downloaded newer image for gcr.io/compute-image-tools/gce_vm_image_import:release
+gcr.io/compute-image-tools/gce_vm_image_import:release
+[import-image]: 2021-03-12T17:29:05Z Creating Google Compute Engine disk from gs://images-bkt-us/random-object-1234
+[inflate]: 2021-03-12T17:29:05Z Validating workflow
+[inflate]: 2021-03-12T17:29:05Z Validating step "setup-disks"
+[inflate]: 2021-03-12T17:29:06Z Validating step "import-virtual-disk"
+[inflate]: 2021-03-12T17:29:06Z Validating step "wait-for-signal"
+[inflate]: 2021-03-12T17:29:06Z Validating step "cleanup"
+[inflate]: 2021-03-12T17:29:06Z Validation Complete
+[inflate]: 2021-03-12T17:29:06Z Workflow Project: ascendant-braid-303513
+[inflate]: 2021-03-12T17:29:06Z Workflow Zone: us-central1-c
+[inflate]: 2021-03-12T17:29:06Z Workflow GCSPath: gs://ascendant-braid-303513-daisy-bkt-us-central1/gce-image-import-2021-03-12T17:29:03Z-qllpn
+[inflate]: 2021-03-12T17:29:06Z Daisy scratch path: https://console.cloud.google.com/storage/browser/ascendant-braid-303513-daisy-bkt-us-central1/gce-image-import-2021-03-12T17:29:03Z-qllpn/daisy-inflate-20210312-17:29:05-1wghm
+[inflate]: 2021-03-12T17:29:06Z Uploading sources
+[inflate]: 2021-03-12T17:29:07Z Running workflow
+[inflate]: 2021-03-12T17:29:07Z Running step "setup-disks" (CreateDisks)
+[inflate.setup-disks]: 2021-03-12T17:29:07Z CreateDisks: Creating disk "disk-importer-inflate-1wghm".
+[inflate.setup-disks]: 2021-03-12T17:29:07Z CreateDisks: Creating disk "disk-qllpn".
+[inflate.setup-disks]: 2021-03-12T17:29:07Z CreateDisks: Creating disk "disk-inflate-scratch-1wghm".
+[inflate]: 2021-03-12T17:29:08Z Step "setup-disks" (CreateDisks) successfully finished.
+[inflate]: 2021-03-12T17:29:08Z Running step "import-virtual-disk" (CreateInstances)
+[inflate.import-virtual-disk]: 2021-03-12T17:29:08Z CreateInstances: Creating instance "inst-importer-inflate-1wghm".
+[inflate]: 2021-03-12T17:29:18Z Step "import-virtual-disk" (CreateInstances) successfully finished.
+[inflate.import-virtual-disk]: 2021-03-12T17:29:18Z CreateInstances: Streaming instance "inst-importer-inflate-1wghm" serial port 1 output to https://storage.cloud.google.com/ascendant-braid-303513-daisy-bkt-us-central1/gce-image-import-2021-03-12T17:29:03Z-qllpn/daisy-inflate-20210312-17:29:05-1wghm/logs/inst-importer-inflate-1wghm-serial-port1.log
+[inflate]: 2021-03-12T17:29:18Z Running step "wait-for-signal" (WaitForInstancesSignal)
+[inflate.wait-for-signal]: 2021-03-12T17:29:18Z WaitForInstancesSignal: Instance "inst-importer-inflate-1wghm": watching serial port 1, SuccessMatch: "ImportSuccess:", FailureMatch: ["ImportFailed:" "WARNING Failed to download metadata script" "Failed to download GCS path" "Worker instance terminated"] (this is not an error), StatusMatch: "Import:".
+[inflate.wait-for-signal]: 2021-03-12T17:29:28Z WaitForInstancesSignal: Instance "inst-importer-inflate-1wghm": StatusMatch found: "Import: Ensuring disk-inflate-scratch-1wghm has capacity of 3 GB in projects/550072179371/zones/us-central1-c."
+[inflate.wait-for-signal]: 2021-03-12T17:29:28Z WaitForInstancesSignal: Instance "inst-importer-inflate-1wghm": StatusMatch found: "Import: /dev/sdb is attached and ready."
+[inflate.wait-for-signal]: 2021-03-12T17:29:48Z WaitForInstancesSignal: Instance "inst-importer-inflate-1wghm": StatusMatch found: "Import: Copied image from gs://images-bkt-us/random-object-1234 to /daisy-scratch/random-object-1234:"
+[inflate.wait-for-signal]: 2021-03-12T17:29:48Z WaitForInstancesSignal: Instance "inst-importer-inflate-1wghm": StatusMatch found: "Import: Importing /daisy-scratch/random-object-1234 of size 2GB to disk-qllpn in projects/550072179371/zones/us-central1-c."
+[inflate.wait-for-signal]: 2021-03-12T17:29:48Z WaitForInstancesSignal: Instance "inst-importer-inflate-1wghm": StatusMatch found: "Import: <serial-output key:'target-size-gb' value:'2'>"
+[inflate.wait-for-signal]: 2021-03-12T17:29:48Z WaitForInstancesSignal: Instance "inst-importer-inflate-1wghm": StatusMatch found: "Import: <serial-output key:'source-size-gb' value:'2'>"
+[inflate.wait-for-signal]: 2021-03-12T17:29:48Z WaitForInstancesSignal: Instance "inst-importer-inflate-1wghm": StatusMatch found: "Import: <serial-output key:'import-file-format' value:'vmdk'>"
+[inflate.wait-for-signal]: 2021-03-12T17:29:48Z WaitForInstancesSignal: Instance "inst-importer-inflate-1wghm": StatusMatch found: "Import: Ensuring disk-qllpn has capacity of 2 GB in projects/550072179371/zones/us-central1-c."
+[inflate.wait-for-signal]: 2021-03-12T17:29:48Z WaitForInstancesSignal: Instance "inst-importer-inflate-1wghm": StatusMatch found: "Import: /dev/sdc is attached and ready."
+[debug]: 2021-03-12T17:29:52Z Started checksum calculation.
+[shadow-disk-checksum]: 2021-03-12T17:29:53Z Validating workflow
+[shadow-disk-checksum]: 2021-03-12T17:29:53Z Validating step "create-disks"
+[shadow-disk-checksum]: 2021-03-12T17:29:53Z Validating step "create-instance"
+[shadow-disk-checksum]: 2021-03-12T17:29:54Z Validating step "wait-for-checksum"
+[shadow-disk-checksum]: 2021-03-12T17:29:54Z Validation Complete
+[shadow-disk-checksum]: 2021-03-12T17:29:54Z Workflow Project: ascendant-braid-303513
+[shadow-disk-checksum]: 2021-03-12T17:29:54Z Workflow Zone: us-central1-c
+[shadow-disk-checksum]: 2021-03-12T17:29:54Z Workflow GCSPath: gs://ascendant-braid-303513-daisy-bkt-us-central1/gce-image-import-2021-03-12T17:29:03Z-qllpn
+[shadow-disk-checksum]: 2021-03-12T17:29:54Z Daisy scratch path: https://console.cloud.google.com/storage/browser/ascendant-braid-303513-daisy-bkt-us-central1/gce-image-import-2021-03-12T17:29:03Z-qllpn/daisy-shadow-disk-checksum-20210312-17:29:53-r3qxv
+[shadow-disk-checksum]: 2021-03-12T17:29:54Z Uploading sources
+[shadow-disk-checksum]: 2021-03-12T17:29:54Z Running workflow
+[shadow-disk-checksum]: 2021-03-12T17:29:54Z Running step "create-disks" (CreateDisks)
+[shadow-disk-checksum.create-disks]: 2021-03-12T17:29:54Z CreateDisks: Creating disk "disk-shadow-disk-checksum-shadow-disk-checksum-r3qxv".
+[shadow-disk-checksum]: 2021-03-12T17:29:55Z Step "create-disks" (CreateDisks) successfully finished.
+[shadow-disk-checksum]: 2021-03-12T17:29:55Z Running step "create-instance" (CreateInstances)
+[shadow-disk-checksum.create-instance]: 2021-03-12T17:29:55Z CreateInstances: Creating instance "inst-shadow-disk-checksum-shadow-disk-checksum-r3qxv".
+[shadow-disk-checksum]: 2021-03-12T17:30:03Z Error running workflow: step "create-instance" run error: operation failed &{ClientOperationId: CreationTimestamp: Description: EndTime:2021-03-12T09:30:02.764-08:00 Error:0xc000454460 HttpErrorMessage:FORBIDDEN HttpErrorStatusCode:403 Id:4817697984863411195 InsertTime:2021-03-12T09:29:56.910-08:00 Kind:compute#operation Name:operation-1615570195674-5bd5a3f9f770d-10b61845-7bf8f2ab OperationType:insert Progress:100 Region: SelfLink:https://www.googleapis.com/compute/v1/projects/ascendant-braid-303513/zones/us-central1-c/operations/operation-1615570195674-5bd5a3f9f770d-10b61845-7bf8f2ab StartTime:2021-03-12T09:29:56.913-08:00 Status:DONE StatusMessage: TargetId:2023576705161370619 TargetLink:https://www.googleapis.com/compute/v1/projects/ascendant-braid-303513/zones/us-central1-c/instances/inst-shadow-disk-checksum-shadow-disk-checksum-r3qxv User:550072179371@cloudbuild.gserviceaccount.com Warnings:[] Zone:https://www.googleapis.com/compute/v1/projects/ascendant-braid-303513/zones/us-central1-c ServerResponse:{HTTPStatusCode:200 Header:map[Cache-Control:[private] Content-Type:[application/json; charset=UTF-8] Date:[Fri, 12 Mar 2021 17:30:03 GMT] Server:[ESF] Vary:[Origin X-Origin Referer] X-Content-Type-Options:[nosniff] X-Frame-Options:[SAMEORIGIN] X-Xss-Protection:[0]]} ForceSendFields:[] NullFields:[]}:
+Code: QUOTA_EXCEEDED
+Message: Quota 'CPUS' exceeded.  Limit: 24.0 in region us-central1.
+[shadow-disk-checksum]: 2021-03-12T17:30:03Z Workflow "shadow-disk-checksum" cleaning up (this may take up to 2 minutes).
+[shadow-disk-checksum]: 2021-03-12T17:30:03Z Workflow "shadow-disk-checksum" finished cleanup.
+[inflate.wait-for-signal]: 2021-03-12T17:30:38Z WaitForInstancesSignal: Instance "inst-importer-inflate-1wghm": StatusMatch found: "Import: <serial-output key:'disk-checksum' value:'5ed32cb4d9d9dd17cba6da1b3903edaa  --d29c6650c73d602034fea42869a545ae  --75a1e608e6f1c50758f4fee5a7d8e3d0  --75a1e608e6f1c50758f4fee5a7d8e3d0  -'>"
+[inflate.wait-for-signal]: 2021-03-12T17:30:38Z WaitForInstancesSignal: Instance "inst-importer-inflate-1wghm": SuccessMatch found "ImportSuccess: Finished import."
+[inflate]: 2021-03-12T17:30:38Z Step "wait-for-signal" (WaitForInstancesSignal) successfully finished.
+[inflate]: 2021-03-12T17:30:38Z Running step "cleanup" (DeleteResources)
+[inflate.cleanup]: 2021-03-12T17:30:38Z DeleteResources: Deleting instance "inst-importer".
+[inflate]: 2021-03-12T17:31:05Z Step "cleanup" (DeleteResources) successfully finished.
+[inflate]: 2021-03-12T17:31:05Z Serial-output value -> disk-checksum:5ed32cb4d9d9dd17cba6da1b3903edaa  --d29c6650c73d602034fea42869a545ae  --75a1e608e6f1c50758f4fee5a7d8e3d0  --75a1e608e6f1c50758f4fee5a7d8e3d0  -
+[inflate]: 2021-03-12T17:31:05Z Serial-output value -> target-size-gb:2
+[inflate]: 2021-03-12T17:31:05Z Serial-output value -> source-size-gb:2
+[inflate]: 2021-03-12T17:31:05Z Serial-output value -> import-file-format:vmdk
+[inflate]: 2021-03-12T17:31:05Z Workflow "inflate" cleaning up (this may take up to 2 minutes).
+[inflate]: 2021-03-12T17:31:07Z Workflow "inflate" finished cleanup.
+[import-image]: 2021-03-12T17:31:07Z Finished creating Google Compute Engine disk
+[import-image]: 2021-03-12T17:31:07Z Creating image "image-dea5f2fb8b6e3826653cfc02ef648c8f8a3be5cb0501aeb75dcd6147"
+PUSH
+DONE
+`,
+			resources: cloudbuildBuildResources{
+				zone: "us-central1-c",
+				computeInstances: []string{
+					"inst-importer-inflate-1wghm",
+					"inst-shadow-disk-checksum-shadow-disk-checksum-r3qxv",
+				},
+				computeDisks: []string{
+					"disk-qllpn",
+					"disk-importer-inflate-1wghm",
+					"disk-inflate-scratch-1wghm",
+					"disk-shadow-disk-checksum-shadow-disk-checksum-r3qxv",
+				},
+				storageCacheDir: struct {
+					bucket string
+					dir    string
+				}{
+					bucket: "ascendant-braid-303513-daisy-bkt-us-central1",
+					dir:    "gce-image-import-2021-03-12T17:29:03Z-qllpn",
+				},
+			},
+		},
+		{
+			buildLog: `starting build "cbf2e886-a81f-4761-9d39-0a03c3579996"
+
+FETCHSOURCE
+BUILD
+Pulling image: gcr.io/compute-image-tools/gce_vm_image_import:release
+release: Pulling from compute-image-tools/gce_vm_image_import
+0b8bbb5f50a4: Pulling fs layer
+7efaa022ad36: Pulling fs layer
+e5303db5f8f9: Pulling fs layer
+688d304ec274: Pulling fs layer
+e969b3a22ab3: Pulling fs layer
+cd7b8272632b: Pulling fs layer
+2175d0ddd745: Pulling fs layer
+69fbd73b475e: Pulling fs layer
+7a5922a992b2: Pulling fs layer
+688d304ec274: Waiting
+e969b3a22ab3: Waiting
+cd7b8272632b: Waiting
+2175d0ddd745: Waiting
+69fbd73b475e: Waiting
+7a5922a992b2: Waiting
+e5303db5f8f9: Verifying Checksum
+e5303db5f8f9: Download complete
+688d304ec274: Download complete
+e969b3a22ab3: Verifying Checksum
+e969b3a22ab3: Download complete
+0b8bbb5f50a4: Verifying Checksum
+0b8bbb5f50a4: Download complete
+cd7b8272632b: Verifying Checksum
+cd7b8272632b: Download complete
+69fbd73b475e: Verifying Checksum
+69fbd73b475e: Download complete
+7a5922a992b2: Verifying Checksum
+7a5922a992b2: Download complete
+7efaa022ad36: Verifying Checksum
+7efaa022ad36: Download complete
+2175d0ddd745: Verifying Checksum
+2175d0ddd745: Download complete
+0b8bbb5f50a4: Pull complete
+7efaa022ad36: Pull complete
+e5303db5f8f9: Pull complete
+688d304ec274: Pull complete
+e969b3a22ab3: Pull complete
+cd7b8272632b: Pull complete
+2175d0ddd745: Pull complete
+69fbd73b475e: Pull complete
+7a5922a992b2: Pull complete
+Digest: sha256:d39e2c0e6a7113d989d292536e9d14e927de838cb21a24c61eb7d44fef1fa51d
+Status: Downloaded newer image for gcr.io/compute-image-tools/gce_vm_image_import:release
+gcr.io/compute-image-tools/gce_vm_image_import:release
+[import-image]: 2021-03-12T16:52:00Z Creating Google Compute Engine disk from gs://images-bkt-us/random-object-1234
+[inflate]: 2021-03-12T16:52:01Z Validating workflow
+[inflate]: 2021-03-12T16:52:01Z Validating step "setup-disks"
+[inflate]: 2021-03-12T16:52:01Z Validating step "import-virtual-disk"
+[inflate]: 2021-03-12T16:52:01Z Validating step "wait-for-signal"
+[inflate]: 2021-03-12T16:52:01Z Validating step "cleanup"
+[inflate]: 2021-03-12T16:52:01Z Validation Complete
+[inflate]: 2021-03-12T16:52:01Z Workflow Project: ascendant-braid-303513
+[inflate]: 2021-03-12T16:52:01Z Workflow Zone: us-central1-c
+[inflate]: 2021-03-12T16:52:01Z Workflow GCSPath: gs://ascendant-braid-303513-daisy-bkt-us-central1/gce-image-import-2021-03-12T16:51:59Z-74mbx
+[inflate]: 2021-03-12T16:52:01Z Daisy scratch path: https://console.cloud.google.com/storage/browser/ascendant-braid-303513-daisy-bkt-us-central1/gce-image-import-2021-03-12T16:51:59Z-74mbx/daisy-inflate-20210312-16:52:01-trs8w
+[inflate]: 2021-03-12T16:52:01Z Uploading sources
+[inflate]: 2021-03-12T16:52:01Z Running workflow
+[inflate]: 2021-03-12T16:52:01Z Running step "setup-disks" (CreateDisks)
+[inflate.setup-disks]: 2021-03-12T16:52:01Z CreateDisks: Creating disk "disk-74mbx".
+[inflate.setup-disks]: 2021-03-12T16:52:01Z CreateDisks: Creating disk "disk-importer-inflate-trs8w".
+[inflate.setup-disks]: 2021-03-12T16:52:01Z CreateDisks: Creating disk "disk-inflate-scratch-trs8w".
+[inflate]: 2021-03-12T16:52:03Z Step "setup-disks" (CreateDisks) successfully finished.
+[inflate]: 2021-03-12T16:52:03Z Running step "import-virtual-disk" (CreateInstances)
+[inflate.import-virtual-disk]: 2021-03-12T16:52:03Z CreateInstances: Creating instance "inst-importer-inflate-trs8w".
+[inflate]: 2021-03-12T16:52:17Z Step "import-virtual-disk" (CreateInstances) successfully finished.
+[inflate.import-virtual-disk]: 2021-03-12T16:52:17Z CreateInstances: Streaming instance "inst-importer-inflate-trs8w" serial port 1 output to https://storage.cloud.google.com/ascendant-braid-303513-daisy-bkt-us-central1/gce-image-import-2021-03-12T16:51:59Z-74mbx/daisy-inflate-20210312-16:52:01-trs8w/logs/inst-importer-inflate-trs8w-serial-port1.log
+[inflate]: 2021-03-12T16:52:17Z Running step "wait-for-signal" (WaitForInstancesSignal)
+[inflate.wait-for-signal]: 2021-03-12T16:52:17Z WaitForInstancesSignal: Instance "inst-importer-inflate-trs8w": watching serial port 1, SuccessMatch: "ImportSuccess:", FailureMatch: ["ImportFailed:" "WARNING Failed to download metadata script" "Failed to download GCS path" "Worker instance terminated"] (this is not an error), StatusMatch: "Import:".
+[inflate.wait-for-signal]: 2021-03-12T16:52:28Z WaitForInstancesSignal: Instance "inst-importer-inflate-trs8w": StatusMatch found: "Import: Ensuring disk-inflate-scratch-trs8w has capacity of 3 GB in projects/550072179371/zones/us-central1-c."
+[inflate.wait-for-signal]: 2021-03-12T16:52:28Z WaitForInstancesSignal: Instance "inst-importer-inflate-trs8w": StatusMatch found: "Import: /dev/sdb is attached and ready."
+[debug]: 2021-03-12T16:52:38Z Started checksum calculation.
+[shadow-disk-checksum]: 2021-03-12T16:52:38Z Validating workflow
+[shadow-disk-checksum]: 2021-03-12T16:52:38Z Validating step "create-disks"
+CANCELLED
+ERROR: context canceled`,
+			resources: cloudbuildBuildResources{
+				zone: "us-central1-c",
+				computeInstances: []string{
+					"inst-importer-inflate-trs8w",
+				},
+				computeDisks: []string{
+					"disk-74mbx",
+					"disk-importer-inflate-trs8w",
+					"disk-inflate-scratch-trs8w",
+				},
+				storageCacheDir: struct {
+					bucket string
+					dir    string
+				}{
+					bucket: "ascendant-braid-303513-daisy-bkt-us-central1",
+					dir:    "gce-image-import-2021-03-12T16:51:59Z-74mbx",
+				},
+			},
+		},
+		{
+			buildLog: `starting build "4f351d2a-5c07-4555-8319-ee7a8e514da1"
+
+FETCHSOURCE
+BUILD
+Pulling image: gcr.io/compute-image-tools/gce_vm_image_import:release
+release: Pulling from compute-image-tools/gce_vm_image_import
+2e1eb53387e5: Pulling fs layer
+95cc589e8a63: Pulling fs layer
+3b6aa88d2880: Pulling fs layer
+4cf16dbc31d9: Pulling fs layer
+45c52af3bf12: Pulling fs layer
+ae477f711ff8: Pulling fs layer
+abf85f8ba3ed: Pulling fs layer
+a06a66dd712b: Pulling fs layer
+e6263716b8ac: Pulling fs layer
+4cf16dbc31d9: Waiting
+45c52af3bf12: Waiting
+ae477f711ff8: Waiting
+abf85f8ba3ed: Waiting
+a06a66dd712b: Waiting
+e6263716b8ac: Waiting
+3b6aa88d2880: Verifying Checksum
+3b6aa88d2880: Download complete
+4cf16dbc31d9: Verifying Checksum
+4cf16dbc31d9: Download complete
+45c52af3bf12: Verifying Checksum
+45c52af3bf12: Download complete
+ae477f711ff8: Verifying Checksum
+ae477f711ff8: Download complete
+95cc589e8a63: Verifying Checksum
+95cc589e8a63: Download complete
+a06a66dd712b: Download complete
+e6263716b8ac: Verifying Checksum
+e6263716b8ac: Download complete
+2e1eb53387e5: Verifying Checksum
+2e1eb53387e5: Download complete
+abf85f8ba3ed: Verifying Checksum
+abf85f8ba3ed: Download complete
+2e1eb53387e5: Pull complete
+95cc589e8a63: Pull complete
+3b6aa88d2880: Pull complete
+4cf16dbc31d9: Pull complete
+45c52af3bf12: Pull complete
+ae477f711ff8: Pull complete
+abf85f8ba3ed: Pull complete
+a06a66dd712b: Pull complete
+e6263716b8ac: Pull complete
+Digest: sha256:90ccc6b8c1239f14690ade311b2a85c6e75931f903c614b4556c1024d54783b5
+Status: Downloaded newer image for gcr.io/compute-image-tools/gce_vm_image_import:release
+gcr.io/compute-image-tools/gce_vm_image_import:release
+[import-image]: 2021-02-17T12:42:09Z Creating Google Compute Engine disk from gs://thozza-images/f32-image.vhd
+[inflate]: 2021-02-17T12:42:09Z Validating workflow
+[inflate]: 2021-02-17T12:42:09Z Validating step "setup-disks"
+[inflate]: 2021-02-17T12:42:10Z Validating step "import-virtual-disk"
+[inflate]: 2021-02-17T12:42:10Z Validating step "wait-for-signal"
+[inflate]: 2021-02-17T12:42:10Z Validating step "cleanup"
+[inflate]: 2021-02-17T12:42:10Z Validation Complete
+[inflate]: 2021-02-17T12:42:10Z Workflow Project: ascendant-braid-303513
+[inflate]: 2021-02-17T12:42:10Z Workflow Zone: europe-west1-b
+[inflate]: 2021-02-17T12:42:10Z Workflow GCSPath: gs://ascendant-braid-303513-daisy-bkt-eu/gce-image-import-2021-02-17T12:42:05Z-lz55d
+[inflate]: 2021-02-17T12:42:10Z Daisy scratch path: https://console.cloud.google.com/storage/browser/ascendant-braid-303513-daisy-bkt-eu/gce-image-import-2021-02-17T12:42:05Z-lz55d/daisy-inflate-20210217-12:42:09-p57zp
+[inflate]: 2021-02-17T12:42:10Z Uploading sources
+[inflate]: 2021-02-17T12:42:11Z Running workflow
+[inflate]: 2021-02-17T12:42:11Z Running step "setup-disks" (CreateDisks)
+[inflate.setup-disks]: 2021-02-17T12:42:11Z CreateDisks: Creating disk "disk-lz55d".
+[inflate.setup-disks]: 2021-02-17T12:42:11Z CreateDisks: Creating disk "disk-importer-inflate-p57zp".
+[inflate.setup-disks]: 2021-02-17T12:42:11Z CreateDisks: Creating disk "disk-inflate-scratch-p57zp".
+[inflate]: 2021-02-17T12:42:13Z Step "setup-disks" (CreateDisks) successfully finished.
+[inflate]: 2021-02-17T12:42:13Z Running step "import-virtual-disk" (CreateInstances)
+[inflate.import-virtual-disk]: 2021-02-17T12:42:13Z CreateInstances: Creating instance "inst-importer-inflate-p57zp".
+[inflate.import-virtual-disk]: 2021-02-17T12:42:23Z CreateInstances: Streaming instance "inst-importer-inflate-p57zp" serial port 1 output to https://storage.cloud.google.com/ascendant-braid-303513-daisy-bkt-eu/gce-image-import-2021-02-17T12:42:05Z-lz55d/daisy-inflate-20210217-12:42:09-p57zp/logs/inst-importer-inflate-p57zp-serial-port1.log
+[inflate]: 2021-02-17T12:42:23Z Step "import-virtual-disk" (CreateInstances) successfully finished.
+[inflate]: 2021-02-17T12:42:23Z Running step "wait-for-signal" (WaitForInstancesSignal)
+[inflate.wait-for-signal]: 2021-02-17T12:42:23Z WaitForInstancesSignal: Instance "inst-importer-inflate-p57zp": watching serial port 1, SuccessMatch: "ImportSuccess:", FailureMatch: ["ImportFailed:" "WARNING Failed to download metadata script" "Failed to download GCS path" "Worker instance terminated"] (this is not an error), StatusMatch: "Import:".
+[inflate.wait-for-signal]: 2021-02-17T12:42:34Z WaitForInstancesSignal: Instance "inst-importer-inflate-p57zp": StatusMatch found: "Import: Ensuring disk-inflate-scratch-p57zp has capacity of 6 GB in projects/550072179371/zones/europe-west1-b."
+[inflate.wait-for-signal]: 2021-02-17T12:42:34Z WaitForInstancesSignal: Instance "inst-importer-inflate-p57zp": StatusMatch found: "Import: /dev/sdb is attached and ready."
+[inflate.wait-for-signal]: 2021-02-17T12:43:43Z WaitForInstancesSignal: Instance "inst-importer-inflate-p57zp": StatusMatch found: "Import: Copied image from gs://thozza-images/f32-image.vhd to /daisy-scratch/f32-image.vhd:"
+[inflate.wait-for-signal]: 2021-02-17T12:43:43Z WaitForInstancesSignal: Instance "inst-importer-inflate-p57zp": StatusMatch found: "Import: Importing /daisy-scratch/f32-image.vhd of size 5GB to disk-lz55d in projects/550072179371/zones/europe-west1-b."
+[inflate.wait-for-signal]: 2021-02-17T12:43:43Z WaitForInstancesSignal: Instance "inst-importer-inflate-p57zp": StatusMatch found: "Import: <serial-output key:'target-size-gb' value:'5'>"
+[inflate.wait-for-signal]: 2021-02-17T12:43:43Z WaitForInstancesSignal: Instance "inst-importer-inflate-p57zp": StatusMatch found: "Import: <serial-output key:'source-size-gb' value:'5'>"
+[inflate.wait-for-signal]: 2021-02-17T12:43:43Z WaitForInstancesSignal: Instance "inst-importer-inflate-p57zp": StatusMatch found: "Import: <serial-output key:'import-file-format' value:'raw'>"
+[inflate.wait-for-signal]: 2021-02-17T12:43:43Z WaitForInstancesSignal: Instance "inst-importer-inflate-p57zp": StatusMatch found: "Import: Ensuring disk-lz55d has capacity of 5 GB in projects/550072179371/zones/europe-west1-b."
+[inflate.wait-for-signal]: 2021-02-17T12:43:43Z WaitForInstancesSignal: Instance "inst-importer-inflate-p57zp": StatusMatch found: "Import: /dev/sdc is attached and ready."
+[inflate.wait-for-signal]: 2021-02-17T12:44:13Z WaitForInstancesSignal: Instance "inst-importer-inflate-p57zp": StatusMatch found: "Import: <serial-output key:'disk-checksum' value:'a6e310ede3ecdc9de88cd402b4709b6a  --20765c2763c49ae251ce0e7762499abf  --75a1e608e6f1c50758f4fee5a7d8e3d0  --75a1e608e6f1c50758f4fee5a7d8e3d0  -'>"
+[inflate.wait-for-signal]: 2021-02-17T12:44:13Z WaitForInstancesSignal: Instance "inst-importer-inflate-p57zp": SuccessMatch found "ImportSuccess: Finished import."
+[inflate]: 2021-02-17T12:44:13Z Step "wait-for-signal" (WaitForInstancesSignal) successfully finished.
+[inflate]: 2021-02-17T12:44:13Z Running step "cleanup" (DeleteResources)
+[inflate.cleanup]: 2021-02-17T12:44:13Z DeleteResources: Deleting instance "inst-importer".
+[inflate]: 2021-02-17T12:44:32Z Step "cleanup" (DeleteResources) successfully finished.
+[inflate]: 2021-02-17T12:44:32Z Serial-output value -> target-size-gb:5
+[inflate]: 2021-02-17T12:44:32Z Serial-output value -> source-size-gb:5
+[inflate]: 2021-02-17T12:44:32Z Serial-output value -> import-file-format:raw
+[inflate]: 2021-02-17T12:44:32Z Serial-output value -> disk-checksum:a6e310ede3ecdc9de88cd402b4709b6a  --20765c2763c49ae251ce0e7762499abf  --75a1e608e6f1c50758f4fee5a7d8e3d0  --75a1e608e6f1c50758f4fee5a7d8e3d0  -
+[inflate]: 2021-02-17T12:44:32Z Workflow "inflate" cleaning up (this may take up to 2 minutes).
+[inflate]: 2021-02-17T12:44:38Z Workflow "inflate" finished cleanup.
+[import-image]: 2021-02-17T12:44:38Z Finished creating Google Compute Engine disk
+[import-image]: 2021-02-17T12:44:38Z Inspecting disk for OS and bootloader
+[inspect]: 2021-02-17T12:44:38Z Validating workflow
+[inspect]: 2021-02-17T12:44:38Z Validating step "run-inspection"
+[inspect]: 2021-02-17T12:44:39Z Validating step "wait-for-signal"
+[inspect]: 2021-02-17T12:44:39Z Validating step "cleanup"
+[inspect]: 2021-02-17T12:44:39Z Validation Complete
+[inspect]: 2021-02-17T12:44:39Z Workflow Project: ascendant-braid-303513
+[inspect]: 2021-02-17T12:44:39Z Workflow Zone: europe-west1-b
+[inspect]: 2021-02-17T12:44:39Z Workflow GCSPath: gs://ascendant-braid-303513-daisy-bkt-eu/gce-image-import-2021-02-17T12:42:05Z-lz55d
+[inspect]: 2021-02-17T12:44:39Z Daisy scratch path: https://console.cloud.google.com/storage/browser/ascendant-braid-303513-daisy-bkt-eu/gce-image-import-2021-02-17T12:42:05Z-lz55d/daisy-inspect-20210217-12:44:38-t6wt4
+[inspect]: 2021-02-17T12:44:39Z Uploading sources
+[inspect]: 2021-02-17T12:44:42Z Running workflow
+[inspect]: 2021-02-17T12:44:42Z Running step "run-inspection" (CreateInstances)
+[inspect.run-inspection]: 2021-02-17T12:44:42Z CreateInstances: Creating instance "run-inspection-inspect-t6wt4".
+[inspect]: 2021-02-17T12:44:51Z Step "run-inspection" (CreateInstances) successfully finished.
+[inspect.run-inspection]: 2021-02-17T12:44:51Z CreateInstances: Streaming instance "run-inspection-inspect-t6wt4" serial port 1 output to https://storage.cloud.google.com/ascendant-braid-303513-daisy-bkt-eu/gce-image-import-2021-02-17T12:42:05Z-lz55d/daisy-inspect-20210217-12:44:38-t6wt4/logs/run-inspection-inspect-t6wt4-serial-port1.log
+[inspect]: 2021-02-17T12:44:51Z Running step "wait-for-signal" (WaitForInstancesSignal)
+[inspect.wait-for-signal]: 2021-02-17T12:44:51Z WaitForInstancesSignal: Instance "run-inspection-inspect-t6wt4": watching serial port 1, SuccessMatch: "Success:", FailureMatch: ["Failed:" "WARNING Failed to download metadata script" "Failed to download GCS path"] (this is not an error), StatusMatch: "Status:".
+[inspect.wait-for-signal]: 2021-02-17T12:45:51Z WaitForInstancesSignal: Instance "run-inspection-inspect-t6wt4": StatusMatch found: "Status: <serial-output key:'inspect_pb' value:'CgkaAjMyKAIwoB84AQ=='>"
+[inspect.wait-for-signal]: 2021-02-17T12:45:51Z WaitForInstancesSignal: Instance "run-inspection-inspect-t6wt4": SuccessMatch found "Success: Done!"
+[inspect]: 2021-02-17T12:45:51Z Step "wait-for-signal" (WaitForInstancesSignal) successfully finished.
+[inspect]: 2021-02-17T12:45:51Z Running step "cleanup" (DeleteResources)
+[inspect.cleanup]: 2021-02-17T12:45:51Z DeleteResources: Deleting instance "run-inspection".
+[inspect]: 2021-02-17T12:46:07Z Step "cleanup" (DeleteResources) successfully finished.
+[inspect]: 2021-02-17T12:46:07Z Serial-output value -> inspect_pb:CgkaAjMyKAIwoB84AQ==
+[inspect]: 2021-02-17T12:46:07Z Workflow "inspect" cleaning up (this may take up to 2 minutes).
+[inspect]: 2021-02-17T12:46:11Z Workflow "inspect" finished cleanup.
+[debug]: 2021-02-17T12:46:11Z Detection results: os_release:{major_version:"32"  architecture:X64  distro_id:FEDORA}  os_count:1
+[import-image]: 2021-02-17T12:46:11Z Inspection result=os_release:{distro:"fedora"  major_version:"32"  architecture:X64  distro_id:FEDORA}  elapsed_time_ms:93747  os_count:1
+[import-image]: 2021-02-17T12:46:13Z Could not detect operating system. Please re-import with the operating system specified. For more information, see https://cloud.google.com/compute/docs/import/importing-virtual-disks#bootable
+ERROR
+ERROR: build step 0 "gcr.io/compute-image-tools/gce_vm_image_import:release" failed: step exited with non-zero status: 1`,
+			resources: cloudbuildBuildResources{
+				zone: "europe-west1-b",
+				computeInstances: []string{
+					"inst-importer-inflate-p57zp",
+					"run-inspection-inspect-t6wt4",
+				},
+				computeDisks: []string{
+					"disk-lz55d",
+					"disk-importer-inflate-p57zp",
+					"disk-inflate-scratch-p57zp",
+				},
+				storageCacheDir: struct {
+					bucket string
+					dir    string
+				}{
+					bucket: "ascendant-braid-303513-daisy-bkt-eu",
+					dir:    "gce-image-import-2021-02-17T12:42:05Z-lz55d",
+				},
+			},
+		},
+	}
+
+	for i, tc := range testCases {
+		t.Run(fmt.Sprintf("log #%d", i), func(t *testing.T) {
+			resources, err := cloudbuildResourcesFromBuildLog(tc.buildLog)
+			require.NoError(t, err)
+			require.NotNil(t, resources)
+
+			require.Equal(t, resources.zone, tc.resources.zone)
+			require.ElementsMatch(t, resources.computeDisks, tc.resources.computeDisks)
+			require.ElementsMatch(t, resources.computeInstances, tc.resources.computeInstances)
+			require.Equal(t, resources.storageCacheDir.bucket, tc.resources.storageCacheDir.bucket)
+			require.Equal(t, resources.storageCacheDir.dir, tc.resources.storageCacheDir.dir)
+		})
+	}
+}
diff --git a/test/cases/api.sh b/test/cases/api.sh
index bf5efa1ea..d664de784 100755
--- a/test/cases/api.sh
+++ b/test/cases/api.sh
@@ -576,7 +576,7 @@ function verifyInAWS() {
   fi
 }
 
-# Verify image in Compute Node on GCP
+# Verify image in Compute Engine on GCP
 function verifyInGCP() {
   # Authenticate
   $GCP_CMD auth activate-service-account --key-file "$GOOGLE_APPLICATION_CREDENTIALS"