From 9b14768902e4c8f0163055bc5fe50338b640b515 Mon Sep 17 00:00:00 2001 From: Nick Kubala Date: Mon, 26 Aug 2019 15:54:32 -0700 Subject: [PATCH] Use active gcloud credentials for executing cloudbuild when available --- go.sum | 1 + pkg/skaffold/build/cluster/sources/gcs.go | 7 ++- pkg/skaffold/build/gcb/cloud_build.go | 36 +++-------- pkg/skaffold/gcp/auth.go | 43 +++++++++++++ pkg/skaffold/gcp/client.go | 76 +++++++++++++++++++++++ pkg/skaffold/sources/upload.go | 8 +-- 6 files changed, 137 insertions(+), 34 deletions(-) create mode 100644 pkg/skaffold/gcp/client.go diff --git a/go.sum b/go.sum index f4c26468483..27b6379b328 100644 --- a/go.sum +++ b/go.sum @@ -349,6 +349,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0 h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.9.0 h1:jbyannxz0XFD3zdjgrSUsaJbgpH4eTrkdhRChkHPfO8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= diff --git a/pkg/skaffold/build/cluster/sources/gcs.go b/pkg/skaffold/build/cluster/sources/gcs.go index 58c2b8dcab0..24e30e4aed3 100644 --- a/pkg/skaffold/build/cluster/sources/gcs.go +++ b/pkg/skaffold/build/cluster/sources/gcs.go @@ -51,7 +51,12 @@ func (g *GCSBucket) Setup(ctx context.Context, out io.Writer, artifact *latest.A color.Default.Fprintln(out, "Uploading sources to", bucket, "GCS bucket") g.tarName = fmt.Sprintf("context-%s.tar.gz", initialTag) - if err := sources.UploadToGCS(ctx, artifact, bucket, g.tarName, dependencies); err != nil { + c, err := gcp.CloudStorageClient() + if err != nil { + return "", errors.Wrap(err, "getting cloud storage client") + } + defer c.Close() + if err := sources.UploadToGCS(ctx, c, artifact, bucket, g.tarName, dependencies); err != nil { return "", errors.Wrap(err, "uploading sources to GCS") } diff --git a/pkg/skaffold/build/gcb/cloud_build.go b/pkg/skaffold/build/gcb/cloud_build.go index 0bb7262db4e..37fbdbb9181 100644 --- a/pkg/skaffold/build/gcb/cloud_build.go +++ b/pkg/skaffold/build/gcb/cloud_build.go @@ -35,7 +35,6 @@ import ( "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/sources" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" - "github.com/GoogleContainerTools/skaffold/pkg/skaffold/version" "github.com/pkg/errors" "github.com/sirupsen/logrus" cloudbuild "google.golang.org/api/cloudbuild/v1" @@ -50,13 +49,12 @@ func (b *Builder) Build(ctx context.Context, out io.Writer, tags tag.ImageTags, } func (b *Builder) buildArtifactWithCloudBuild(ctx context.Context, out io.Writer, artifact *latest.Artifact, tag string) (string, error) { - cbclient, err := cloudbuild.NewService(ctx) + cbclient, err := gcp.CloudBuildClient() if err != nil { return "", errors.Wrap(err, "getting cloudbuild client") } - cbclient.UserAgent = version.UserAgent() - c, err := cstorage.NewClient(ctx) + c, err := gcp.CloudStorageClient() if err != nil { return "", errors.Wrap(err, "getting cloud storage client") } @@ -75,10 +73,10 @@ func (b *Builder) buildArtifactWithCloudBuild(ctx context.Context, out io.Writer cbBucket := fmt.Sprintf("%s%s", projectID, constants.GCSBucketSuffix) buildObject := fmt.Sprintf("source/%s-%s.tar.gz", projectID, util.RandomID()) - if err := b.createBucketIfNotExists(ctx, projectID, cbBucket); err != nil { + if err := b.createBucketIfNotExists(ctx, c, projectID, cbBucket); err != nil { return "", errors.Wrap(err, "creating bucket if not exists") } - if err := b.checkBucketProjectCorrect(ctx, projectID, cbBucket); err != nil { + if err := b.checkBucketProjectCorrect(ctx, c, projectID, cbBucket); err != nil { return "", errors.Wrap(err, "checking bucket is in correct project") } @@ -88,7 +86,7 @@ func (b *Builder) buildArtifactWithCloudBuild(ctx context.Context, out io.Writer } color.Default.Fprintf(out, "Pushing code to gs://%s/%s\n", cbBucket, buildObject) - if err := sources.UploadToGCS(ctx, artifact, cbBucket, buildObject, dependencies); err != nil { + if err := sources.UploadToGCS(ctx, c, artifact, cbBucket, buildObject, dependencies); err != nil { return "", errors.Wrap(err, "uploading source tarball") } @@ -136,7 +134,7 @@ watch: return "", errors.Wrap(err, "getting build status") } - r, err := b.getLogs(ctx, offset, cbBucket, logsObject) + r, err := b.getLogs(ctx, c, offset, cbBucket, logsObject) if err != nil { return "", errors.Wrap(err, "getting logs") } @@ -198,13 +196,7 @@ func getDigest(b *cloudbuild.Build, defaultToTag string) (string, error) { return docker.RemoteDigest(defaultToTag, nil) } -func (b *Builder) getLogs(ctx context.Context, offset int64, bucket, objectName string) (io.ReadCloser, error) { - c, err := cstorage.NewClient(ctx) - if err != nil { - return nil, errors.Wrap(err, "getting storage client") - } - defer c.Close() - +func (b *Builder) getLogs(ctx context.Context, c *cstorage.Client, offset int64, bucket, objectName string) (io.ReadCloser, error) { r, err := c.Bucket(bucket).Object(objectName).NewRangeReader(ctx, offset, -1) if err != nil { if gerr, ok := err.(*googleapi.Error); ok { @@ -224,11 +216,7 @@ func (b *Builder) getLogs(ctx context.Context, offset int64, bucket, objectName return r, nil } -func (b *Builder) checkBucketProjectCorrect(ctx context.Context, projectID, bucket string) error { - c, err := cstorage.NewClient(ctx) - if err != nil { - return errors.Wrap(err, "getting storage client") - } +func (b *Builder) checkBucketProjectCorrect(ctx context.Context, c *cstorage.Client, projectID, bucket string) error { it := c.Buckets(ctx, projectID) // Set the prefix to the bucket we're looking for to only return that bucket and buckets with that prefix // that we'll filter further later on @@ -248,12 +236,8 @@ func (b *Builder) checkBucketProjectCorrect(ctx context.Context, projectID, buck } } -func (b *Builder) createBucketIfNotExists(ctx context.Context, projectID, bucket string) error { - c, err := cstorage.NewClient(ctx) - if err != nil { - return errors.Wrap(err, "getting storage client") - } - defer c.Close() +func (b *Builder) createBucketIfNotExists(ctx context.Context, c *cstorage.Client, projectID, bucket string) error { + var err error _, err = c.Bucket(bucket).Attrs(ctx) diff --git a/pkg/skaffold/gcp/auth.go b/pkg/skaffold/gcp/auth.go index 694e439abbb..5ea360b2f36 100644 --- a/pkg/skaffold/gcp/auth.go +++ b/pkg/skaffold/gcp/auth.go @@ -17,11 +17,23 @@ limitations under the License. package gcp import ( + "context" + "encoding/json" "os/exec" "strings" + "sync" + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" "github.com/docker/cli/cli/config/configfile" + "github.com/pkg/errors" "github.com/sirupsen/logrus" + "golang.org/x/oauth2/google" +) + +var ( + creds *google.Credentials + credsOnce sync.Once + credsErr error ) // AutoConfigureGCRCredentialHelper automatically adds the `gcloud` credential helper @@ -50,3 +62,34 @@ func AutoConfigureGCRCredentialHelper(cf *configfile.ConfigFile, registry string } cf.CredentialHelpers[registry] = "gcloud" } + +func activeUserCredentials() (*google.Credentials, error) { + credsOnce.Do(func() { + cmd := exec.Command("gcloud", "auth", "print-access-token", "--format=json") + body, err := util.RunCmdOut(cmd) + if err != nil { + credsErr = errors.Wrap(err, "retrieving gcloud access token") + return + } + jsonCreds := make(map[string]interface{}) + json.Unmarshal(body, &jsonCreds) + jsonCreds["type"] = "authorized_user" + body, _ = json.Marshal(jsonCreds) + + c, err := google.CredentialsFromJSON(context.Background(), body) + if err != nil { + logrus.Infof("unable to retrieve google creds: %v", err) + logrus.Infof("falling back to application default credentials") + return + } + _, err = c.TokenSource.Token() + if err != nil { + logrus.Infof("unable to retrieve token: %v", err) + logrus.Infof("falling back to application default credentials") + return + } + creds = c + }) + + return creds, credsErr +} diff --git a/pkg/skaffold/gcp/client.go b/pkg/skaffold/gcp/client.go new file mode 100644 index 00000000000..b8e68a1748b --- /dev/null +++ b/pkg/skaffold/gcp/client.go @@ -0,0 +1,76 @@ +/* +Copyright 2019 The Skaffold Authors + +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 gcp + +import ( + "context" + "sync" + + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/version" + + cstorage "cloud.google.com/go/storage" + "github.com/pkg/errors" + cloudbuild "google.golang.org/api/cloudbuild/v1" + "google.golang.org/api/option" +) + +var ( + cbclient *cloudbuild.Service + cbOnce sync.Once + cbErr error +) + +// CloudBuildClient returns an authenticated client for interacting with +// the Google Cloud Build API. This client is created once and cached +// for repeated use in Skaffold. +func CloudBuildClient() (*cloudbuild.Service, error) { + cbOnce.Do(func() { + var options []option.ClientOption + var err error + creds, cErr := activeUserCredentials() + if cErr == nil && creds != nil { + options = append(options, option.WithCredentials(creds)) + } + + c, err := cloudbuild.NewService(context.Background(), options...) + if err != nil { + cbErr = err + return + } + c.UserAgent = version.UserAgent() + cbclient = c + }) + + return cbclient, cbErr +} + +// CloudStorageClient returns an authenticated client for interacting with +// the Google Cloud Storage API. This client is not cached by Skaffold, +// because it needs to be closed each time it is done being used by the caller. +func CloudStorageClient() (*cstorage.Client, error) { + var options []option.ClientOption + var err error + creds, cErr := activeUserCredentials() + if cErr == nil && creds != nil { + options = append(options, option.WithCredentials(creds)) + } + c, err := cstorage.NewClient(context.Background(), options...) + if err != nil { + return nil, errors.Wrap(err, "getting cloud storage client") + } + return c, nil +} diff --git a/pkg/skaffold/sources/upload.go b/pkg/skaffold/sources/upload.go index 2c2d44d3738..a4c8bf8b1be 100644 --- a/pkg/skaffold/sources/upload.go +++ b/pkg/skaffold/sources/upload.go @@ -36,13 +36,7 @@ func TarGz(ctx context.Context, w io.Writer, a *latest.Artifact, dependencies [] } // UploadToGCS uploads the artifact's sources to a GCS bucket. -func UploadToGCS(ctx context.Context, a *latest.Artifact, bucket, objectName string, dependencies []string) error { - c, err := cstorage.NewClient(ctx) - if err != nil { - return errors.Wrap(err, "creating GCS client") - } - defer c.Close() - +func UploadToGCS(ctx context.Context, c *cstorage.Client, a *latest.Artifact, bucket, objectName string, dependencies []string) error { w := c.Bucket(bucket).Object(objectName).NewWriter(ctx) if err := TarGz(ctx, w, a, dependencies); err != nil { return errors.Wrap(err, "uploading targz to google storage")