blob: c74aacc61849c11d524f05f974d6741ed12f6000 [file] [log] [blame]
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
// Copied from github.com/broady/cdbuild.
// TODO(adg): clean this up.
package main
import (
"archive/tar"
"compress/gzip"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"time"
cstorage "cloud.google.com/go/storage"
"golang.org/x/net/context"
"golang.org/x/oauth2/google"
"google.golang.org/api/cloudbuild/v1"
"google.golang.org/api/googleapi"
"google.golang.org/api/storage/v1"
)
func cdbuild(dir, projectID, name, pkgPath string) error {
stagingBucket := projectID + "-cdbuild"
buildObject := fmt.Sprintf("build/%s-%s.tar.gz", name, randomID())
ctx := context.Background()
hc, err := google.DefaultClient(ctx, storage.CloudPlatformScope)
if err != nil {
return fmt.Errorf("Could not get authenticated HTTP client: %v", err)
}
log.Printf("Pushing code to gs://%s/%s", stagingBucket, buildObject)
if err := uploadTar(ctx, dir, hc, stagingBucket, buildObject); err != nil {
return fmt.Errorf("Could not upload source: %v", err)
}
api, err := cloudbuild.New(hc)
if err != nil {
return fmt.Errorf("Could not get cloudbuild client: %v", err)
}
var steps []*cloudbuild.BuildStep
if pkgPath != "" {
steps = append(steps, &cloudbuild.BuildStep{
Name: "gcr.io/" + projectID + "/cloudbuild",
Args: []string{"install", pkgPath},
})
}
steps = append(steps, &cloudbuild.BuildStep{
Name: "gcr.io/cloud-builders/docker",
Args: []string{"build", "--tag=gcr.io/" + projectID + "/" + name, "."},
})
call := api.Projects.Builds.Create(projectID, &cloudbuild.Build{
LogsBucket: stagingBucket,
Source: &cloudbuild.Source{
StorageSource: &cloudbuild.StorageSource{
Bucket: stagingBucket,
Object: buildObject,
},
},
Steps: steps,
Images: []string{"gcr.io/" + projectID + "/" + name},
})
op, err := call.Context(ctx).Do()
if err != nil {
if gerr, ok := err.(*googleapi.Error); ok {
if gerr.Code == 404 {
// HACK(cbro): the API does not return a good error if the API is not enabled.
fmt.Fprintln(os.Stderr, "Could not create build. It's likely the Cloud Container Builder API is not enabled.")
fmt.Fprintf(os.Stderr, "Go here to enable it: https://console.cloud.google.com/apis/api/cloudbuild.googleapis.com/overview?project=%s\n", projectID)
os.Exit(1)
}
}
return fmt.Errorf("Could not create build: %#v", err)
}
remoteID, err := getBuildID(op)
if err != nil {
return fmt.Errorf("Could not get build ID from op: %v", err)
}
log.Printf("Logs at https://console.cloud.google.com/m/cloudstorage/b/%s/o/log-%s.txt", stagingBucket, remoteID)
fail := false
for {
b, err := api.Projects.Builds.Get(projectID, remoteID).Do()
if err != nil {
return fmt.Errorf("Could not get build status: %v", err)
}
if s := b.Status; s != "WORKING" && s != "QUEUED" {
if b.Status == "FAILURE" {
fail = true
}
log.Printf("Build status: %v", s)
break
}
time.Sleep(time.Second)
}
c, err := cstorage.NewClient(ctx)
if err != nil {
return fmt.Errorf("Could not make Cloud storage client: %v", err)
}
defer c.Close()
if err := c.Bucket(stagingBucket).Object(buildObject).Delete(ctx); err != nil {
return fmt.Errorf("Could not delete source tar.gz: %v", err)
}
log.Print("Cleaned up.")
if fail {
return fmt.Errorf("cdbuild failed")
}
return nil
}
// HACK: workaround for lack of type for "Metadata" field.
func getBuildID(op *cloudbuild.Operation) (string, error) {
if op.Metadata == nil {
return "", errors.New("missing Metadata in operation")
}
var buildMeta cloudbuild.BuildOperationMetadata
if err := json.Unmarshal([]byte(op.Metadata), &buildMeta); err != nil {
return "", err
}
if buildMeta.Build == nil {
return "", errors.New("missing Build in operation metadata")
}
return buildMeta.Build.Id, nil
}
func uploadTar(ctx context.Context, root string, hc *http.Client, bucket string, objectName string) error {
c, err := cstorage.NewClient(ctx)
if err != nil {
return err
}
defer c.Close()
w := c.Bucket(bucket).Object(objectName).NewWriter(ctx)
gzw := gzip.NewWriter(w)
tw := tar.NewWriter(gzw)
if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if path == root {
return nil
}
relpath, err := filepath.Rel(root, path)
if err != nil {
return err
}
info = renamingFileInfo{info, relpath}
hdr, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
rel, err := filepath.Rel(root, path)
if err != nil {
return err
}
hdr.Name = rel
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if info.IsDir() {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(tw, f)
return err
}); err != nil {
w.CloseWithError(err)
return err
}
if err := tw.Close(); err != nil {
w.CloseWithError(err)
return err
}
if err := gzw.Close(); err != nil {
w.CloseWithError(err)
return err
}
return w.Close()
}
type renamingFileInfo struct {
os.FileInfo
name string
}
func (fi renamingFileInfo) Name() string {
return fi.name
}
func randomID() string {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
return fmt.Sprintf("%x", b)
}