github.com/SupersunnySea/draft@v0.16.0/pkg/builder/azure/builder.go (about)

     1  package azure
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/url"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/Azure/azure-storage-blob-go/2016-05-31/azblob"
    15  	"github.com/Azure/draft/pkg/azure/blob"
    16  	"github.com/Azure/draft/pkg/azure/containerregistry"
    17  	"github.com/Azure/draft/pkg/builder"
    18  	"github.com/Azure/go-autorest/autorest/adal"
    19  	azurecli "github.com/Azure/go-autorest/autorest/azure/cli"
    20  	"github.com/Azure/go-autorest/autorest/to"
    21  	"github.com/golang/glog"
    22  	"golang.org/x/net/context"
    23  )
    24  
    25  // Builder contains information about the build environment
    26  type Builder struct {
    27  	RegistryClient containerregistry.RegistriesClient
    28  	BuildsClient   containerregistry.BuildsClient
    29  	AdalToken      adal.Token
    30  	Subscription   azurecli.Subscription
    31  }
    32  
    33  // Build builds the docker image.
    34  func (b *Builder) Build(ctx context.Context, app *builder.AppContext, out chan<- *builder.Summary) (err error) {
    35  	const stageDesc = "Building Docker Image"
    36  
    37  	defer builder.Complete(app.ID, stageDesc, out, &err)
    38  	summary := builder.Summarize(app.ID, stageDesc, out)
    39  
    40  	// notify that particular stage has started.
    41  	summary("started", builder.SummaryStarted)
    42  
    43  	msgc := make(chan string)
    44  	errc := make(chan error)
    45  	go func() {
    46  		defer func() {
    47  			close(msgc)
    48  			close(errc)
    49  		}()
    50  		// the azure SDK wants only the name of the registry rather than the full registry URL
    51  		registryName := getRegistryName(app.Ctx.Env.Registry)
    52  		// first, upload the tarball to the upload storage URL given to us by acr build
    53  		sourceUploadDefinition, err := b.RegistryClient.GetBuildSourceUploadURL(ctx, app.Ctx.Env.ResourceGroupName, registryName)
    54  		if err != nil {
    55  			errc <- fmt.Errorf("Could not retrieve acr build's upload URL: %v", err)
    56  			return
    57  		}
    58  		u, err := url.Parse(*sourceUploadDefinition.UploadURL)
    59  		if err != nil {
    60  			errc <- fmt.Errorf("Could not parse blob upload URL: %v", err)
    61  			return
    62  		}
    63  
    64  		blockBlobService := azblob.NewBlockBlobURL(*u, azblob.NewPipeline(azblob.NewAnonymousCredential(), azblob.PipelineOptions{}))
    65  		// Upload the application tarball to acr build
    66  		_, err = blockBlobService.PutBlob(ctx, bytes.NewReader(app.Ctx.Archive), azblob.BlobHTTPHeaders{ContentType: "application/gzip"}, azblob.Metadata{}, azblob.BlobAccessConditions{})
    67  		if err != nil {
    68  			errc <- fmt.Errorf("Could not upload docker context to acr build: %v", err)
    69  			return
    70  		}
    71  
    72  		var imageNames []string
    73  		for i := range app.Images {
    74  			imageNameParts := strings.Split(app.Images[i], ":")
    75  			// get the tag name from the image name
    76  			imageNames = append(imageNames, fmt.Sprintf("%s:%s", app.Ctx.Env.Name, imageNameParts[len(imageNameParts)-1]))
    77  		}
    78  
    79  		var args []containerregistry.BuildArgument
    80  
    81  		// TODO: once the API includes this as default, remove it
    82  		buildArgType := "DockerBuildArgument"
    83  		for k := range app.Ctx.Env.ImageBuildArgs {
    84  			name := k
    85  			value := app.Ctx.Env.ImageBuildArgs[k]
    86  			arg := containerregistry.BuildArgument{
    87  				Type:  &buildArgType,
    88  				Name:  &name,
    89  				Value: &value,
    90  			}
    91  			args = append(args, arg)
    92  		}
    93  
    94  		req := containerregistry.QuickBuildRequest{
    95  			ImageNames:     to.StringSlicePtr(imageNames),
    96  			SourceLocation: sourceUploadDefinition.RelativePath,
    97  			BuildArguments: &args,
    98  			IsPushEnabled:  to.BoolPtr(true),
    99  			Timeout:        to.Int32Ptr(600),
   100  			Platform: &containerregistry.PlatformProperties{
   101  				// TODO: make this configurable once ACR build supports windows containers
   102  				OsType: containerregistry.Linux,
   103  				// NB: CPU isn't required right now, possibly want to make this configurable
   104  				// It'll actually default to 2 from the server
   105  				// CPU: to.Int32Ptr(1),
   106  			},
   107  			// TODO: make this configurable
   108  			DockerFilePath: to.StringPtr("Dockerfile"),
   109  			Type:           containerregistry.TypeQuickBuild,
   110  		}
   111  		bas, ok := req.AsBasicQueueBuildRequest()
   112  		if !ok {
   113  			errc <- errors.New("Failed to create quick build request")
   114  			return
   115  		}
   116  		future, err := b.RegistryClient.QueueBuild(ctx, app.Ctx.Env.ResourceGroupName, registryName, bas)
   117  		if err != nil {
   118  			errc <- fmt.Errorf("Could not while queue acr build: %v", err)
   119  			return
   120  		}
   121  
   122  		if err := future.WaitForCompletion(ctx, b.RegistryClient.Client); err != nil {
   123  			errc <- fmt.Errorf("Could not wait for acr build to complete: %v", err)
   124  			return
   125  		}
   126  
   127  		fin, err := future.Result(b.RegistryClient)
   128  		if err != nil {
   129  			errc <- fmt.Errorf("Could not retrieve acr build future result: %v", err)
   130  			return
   131  		}
   132  
   133  		logResult, err := b.BuildsClient.GetLogLink(ctx, app.Ctx.Env.ResourceGroupName, registryName, *fin.BuildID)
   134  		if err != nil {
   135  			errc <- fmt.Errorf("Could not retrieve acr build logs: %v", err)
   136  			return
   137  		}
   138  
   139  		if *logResult.LogLink == "" {
   140  			errc <- errors.New("Unable to create a link to the logs: no link found")
   141  			return
   142  		}
   143  
   144  		blobURL := blob.GetAppendBlobURL(*logResult.LogLink)
   145  
   146  		// Used for progress reporting to report the total number of bytes being downloaded.
   147  		var contentLength int64
   148  		rs := azblob.NewDownloadStream(ctx,
   149  			// We pass more than "blobUrl.GetBlob" here so we can capture the blob's full
   150  			// content length on the very first internal call to Read.
   151  			func(ctx context.Context, blobRange azblob.BlobRange, ac azblob.BlobAccessConditions, rangeGetContentMD5 bool) (*azblob.GetResponse, error) {
   152  				for {
   153  					properties, err := blobURL.GetPropertiesAndMetadata(ctx, ac)
   154  					if err != nil {
   155  						// retry if the blob doesn't exist yet
   156  						if strings.Contains(err.Error(), "The specified blob does not exist.") {
   157  							continue
   158  						}
   159  						return nil, err
   160  					}
   161  					// retry if the blob hasn't "completed"
   162  					if !blobComplete(properties.NewMetadata()) {
   163  						continue
   164  					}
   165  					break
   166  				}
   167  				resp, err := blobURL.GetBlob(ctx, blobRange, ac, rangeGetContentMD5)
   168  				if err != nil {
   169  					return nil, err
   170  				}
   171  				if contentLength == 0 {
   172  					// If 1st successful Get, record blob's full size for progress reporting
   173  					contentLength = resp.ContentLength()
   174  				}
   175  				return resp, nil
   176  			},
   177  			azblob.DownloadStreamOptions{})
   178  		defer rs.Close()
   179  
   180  		_, err = io.Copy(app.Log, rs)
   181  		if err != nil {
   182  			errc <- fmt.Errorf("Could not stream acr build logs: %v", err)
   183  			return
   184  		}
   185  
   186  		return
   187  
   188  	}()
   189  	for msgc != nil || errc != nil {
   190  		select {
   191  		case msg, ok := <-msgc:
   192  			if !ok {
   193  				msgc = nil
   194  				continue
   195  			}
   196  			summary(msg, builder.SummaryLogging)
   197  		case err, ok := <-errc:
   198  			if !ok {
   199  				errc = nil
   200  				continue
   201  			}
   202  			return err
   203  		default:
   204  			summary("ongoing", builder.SummaryOngoing)
   205  			time.Sleep(time.Second)
   206  		}
   207  	}
   208  	return nil
   209  }
   210  
   211  // Push pushes the results of Build to the image repository.
   212  func (b *Builder) Push(ctx context.Context, app *builder.AppContext, out chan<- *builder.Summary) (err error) {
   213  	// no-op: acr build pushes to the registry through the quickbuild request
   214  	const stageDesc = "Building Docker Image"
   215  	builder.Complete(app.ID, stageDesc, out, &err)
   216  	return nil
   217  }
   218  
   219  // AuthToken retrieves the auth token for the given image.
   220  func (b *Builder) AuthToken(ctx context.Context, app *builder.AppContext) (string, error) {
   221  	dockerAuth, err := b.getACRDockerEntryFromARMToken(app.Ctx.Env.Registry)
   222  	if err != nil {
   223  		return "", err
   224  	}
   225  	buf, err := json.Marshal(dockerAuth)
   226  	return base64.StdEncoding.EncodeToString(buf), err
   227  }
   228  
   229  func getRegistryName(registry string) string {
   230  	return strings.TrimSuffix(registry, ".azurecr.io")
   231  }
   232  
   233  func blobComplete(metadata azblob.Metadata) bool {
   234  	for k := range metadata {
   235  		if strings.ToLower(k) == "complete" {
   236  			return true
   237  		}
   238  	}
   239  	return false
   240  }
   241  
   242  func (b *Builder) getACRDockerEntryFromARMToken(loginServer string) (*builder.DockerConfigEntryWithAuth, error) {
   243  	accessToken := b.AdalToken.OAuthToken()
   244  
   245  	directive, err := containerregistry.ReceiveChallengeFromLoginServer(loginServer)
   246  	if err != nil {
   247  		return nil, fmt.Errorf("failed to receive challenge: %s", err)
   248  	}
   249  
   250  	registryRefreshToken, err := containerregistry.PerformTokenExchange(
   251  		loginServer, directive, b.Subscription.TenantID, accessToken)
   252  	if err != nil {
   253  		return nil, fmt.Errorf("failed to perform token exchange: %s", err)
   254  	}
   255  
   256  	glog.V(4).Infof("adding ACR docker config entry for: %s", loginServer)
   257  	return &builder.DockerConfigEntryWithAuth{
   258  		Username: containerregistry.DockerTokenLoginUsernameGUID,
   259  		Password: registryRefreshToken,
   260  	}, nil
   261  }