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 }