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