OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 /* | |
6 Package cipd implements client side of Chrome Infra Package Deployer. | |
7 | |
8 TODO: write more. | |
9 | |
10 Binary package file format (in free form representation): | |
11 <binary package> := <zipped data> | |
12 <zipped data> := DeterministicZip(<all input files> + <manifest json>) | |
13 <manifest json> := File{ | |
14 name: ".cipdpkg/manifest.json", | |
15 data: JSON({ | |
16 "FormatVersion": "1", | |
17 "PackageName": <name of the package> | |
18 }), | |
19 } | |
20 DeterministicZip = zip archive with deterministic ordering of files and stripp ed timestamps | |
21 | |
22 Main package data (<zipped data> above) is deterministic, meaning its content | |
23 depends only on inputs used to built it (byte to byte): contents and names of | |
24 all files added to the package (plus 'executable' file mode bit) and a package | |
25 name (and all other data in the manifest). | |
26 | |
27 Binary package data MUST NOT depend on a timestamp, hostname of machine that | |
28 built it, revision of the source code it was built from, etc. All that | |
29 information will be distributed as a separate metadata packet associated with | |
30 the package when it gets uploaded to the server. | |
31 | |
32 TODO: expand more when there's server-side package data model (labels | |
33 and stuff). | |
34 */ | |
35 package cipd | |
36 | |
37 import ( | |
38 "bufio" | |
39 "errors" | |
40 "fmt" | |
41 "io" | |
42 "io/ioutil" | |
43 "net/http" | |
44 "os" | |
45 "path/filepath" | |
46 "strings" | |
47 "time" | |
48 | |
49 "infra/libs/build" | |
50 "infra/libs/logging" | |
51 | |
52 "infra/tools/cipd/common" | |
53 "infra/tools/cipd/local" | |
54 ) | |
55 | |
56 // PackageACLChangeAction defines a flavor of PackageACLChange. | |
57 type PackageACLChangeAction string | |
58 | |
59 const ( | |
60 // GrantRole is used in PackageACLChange to request a role to be granted . | |
61 GrantRole PackageACLChangeAction = "GRANT" | |
62 // RevokeRole is used in PackageACLChange to request a role to be revoke d. | |
63 RevokeRole PackageACLChangeAction = "REVOKE" | |
64 | |
65 // CASFinalizationTimeout is how long to wait for CAS service to finaliz e the upload. | |
66 CASFinalizationTimeout = 1 * time.Minute | |
67 // TagAttachTimeout is how long to wait for instance to be processed whe n attaching tags. | |
68 TagAttachTimeout = 1 * time.Minute | |
69 | |
70 // UserAgent is HTTP user agent string for CIPD client. | |
71 UserAgent = "cipd 1.0" | |
72 | |
73 // ProdServiceURL is URL of a backend to connect to if client is build w ith +release tag. | |
74 ProdServiceURL = "https://chrome-infra-packages.appspot.com" | |
75 // TestingServiceURL is URL of a backend to connect to if client is buil d without +release tag. | |
76 TestingServiceURL = "https://chrome-infra-packages-dev.appspot.com" | |
77 ) | |
78 | |
79 var ( | |
80 // ErrFinalizationTimeout is returned if CAS service can not finalize up load fast enough. | |
81 ErrFinalizationTimeout = errors.New("Timeout while waiting for CAS servi ce to finalize the upload") | |
82 // ErrBadUpload is returned when a package file is uploaded, but servers asks us to upload it again. | |
83 ErrBadUpload = errors.New("Package file is uploaded, but servers asks us to upload it again") | |
84 // ErrBadUploadSession is returned by UploadToCAS if provided UploadSess ion is not valid. | |
85 ErrBadUploadSession = errors.New("UploadURL must be set if UploadSession ID is used") | |
86 // ErrUploadSessionDied is returned by UploadToCAS if upload session sud denly disappeared. | |
87 ErrUploadSessionDied = errors.New("Upload session is unexpectedly missin g") | |
88 // ErrNoUploadSessionID is returned by UploadToCAS if server didn't prov ide upload session ID. | |
89 ErrNoUploadSessionID = errors.New("Server didn't provide upload session ID") | |
90 // ErrAttachTagsTimeout is returned when service refuses to accept tags for a long time. | |
91 ErrAttachTagsTimeout = errors.New("Timeout while attaching tags") | |
92 // ErrDownloadError is returned by FetchInstance on download errors. | |
93 ErrDownloadError = errors.New("Failed to download the package file after multiple attempts") | |
94 // ErrUploadError is returned by RegisterInstance and UploadToCAS on upl oad errors. | |
95 ErrUploadError = errors.New("Failed to upload the package file after mul tiple attempts") | |
96 // ErrAccessDenined is returned by calls talking to backend on 401 or 40 3 HTTP errors. | |
97 ErrAccessDenined = errors.New("Access denied (not authenticated or not e nough permissions)") | |
98 // ErrBackendInaccessible is returned by calls talking to backed if it d oesn't response. | |
99 ErrBackendInaccessible = errors.New("Request to the backend failed after multiple attempts") | |
nodir
2015/05/12 19:09:20
In general, it is cool to have each and every erro
Vadim Sh.
2015/05/12 23:25:03
Some errors are not constants (e.g. errors that wr
| |
100 ) | |
101 | |
102 // HTTPClientFactory lazily creates http.Client to use for making requests. | |
103 type HTTPClientFactory func() (*http.Client, error) | |
104 | |
105 // Client provides high level CIPD client interface. | |
nodir
2015/05/12 19:09:20
nit: high-level
Vadim Sh.
2015/05/12 23:25:03
Done.
| |
106 type Client struct { | |
107 // ServiceURL is root URL of the backend service, or "" to use default s ervice. | |
nodir
2015/05/12 19:09:20
nit: the default service
Vadim Sh.
2015/05/12 23:25:02
It wasn't correct. Removed.
| |
108 ServiceURL string | |
109 // Log is a logger to use for logs, default is logging.DefaultLogger. | |
110 Log logging.Logger | |
111 // AuthenticatedClientFactory lazily creates http.Client to use for maki ng RPC requests. | |
112 AuthenticatedClientFactory HTTPClientFactory | |
113 // AnonymousClientFactory lazily creates http.Client to use for making r equests to storage. | |
114 AnonymousClientFactory HTTPClientFactory | |
115 // UserAgent is put into User-Agent HTTP header with each request. | |
116 UserAgent string | |
117 | |
118 // clock provides current time and ability to sleep. | |
119 clock clock | |
120 // remote knows how to call backend REST API. | |
121 remote remote | |
122 // storage knows how upload and download raw binaries using signed URLs. | |
nodir
2015/05/12 19:09:20
nit: how to upload
Vadim Sh.
2015/05/12 23:25:03
Done.
| |
123 storage storage | |
124 | |
125 // authClient is lazily created http.Client to use to make authenticated requests. | |
nodir
2015/05/12 19:09:20
revisit this comment
Vadim Sh.
2015/05/12 23:25:03
What do you not like about this comment?
nodir
2015/05/12 23:35:54
This is better
| |
126 authClient *http.Client | |
127 // anonClient is lazily created http.Client to use to make anonymous req uests. | |
nodir
2015/05/12 19:09:20
this too
| |
128 anonClient *http.Client | |
129 } | |
130 | |
131 // PackageACL is per package path per role access control list that is a part of | |
132 // larger overall ACL: ACL for package "a/b/c" is a union of PackageACLs for "a" | |
133 // "a/b" and "a/b/c". | |
134 type PackageACL struct { | |
135 // PackagePath is a package subpath this ACL is defined for. | |
136 PackagePath string | |
137 // Role is a role that listed users have, e.g. 'READER', 'WRITER', ... | |
138 Role string | |
nodir
2015/05/12 19:09:20
why not type Role? since you use a special type Ac
Vadim Sh.
2015/05/12 23:25:03
Action is conceptually an enum with only two valid
nodir
2015/05/12 23:35:53
It is not anything, it is READER, WRITER, OWNER, i
Vadim Sh.
2015/05/12 23:48:45
Yes, for now. With introduction of refs there will
nodir
2015/05/13 01:25:28
IMO a role should not identify the resource. I thi
Vadim Sh.
2015/05/13 03:08:06
I'll consider this when I'll be implementing ACL f
| |
139 // Principals list users and groups granted the role. | |
140 Principals []string | |
141 // ModifiedBy specifies who modified the list the last time. | |
142 ModifiedBy string | |
143 // ModifiedTs is a timestamp when the list was modified the last time. | |
144 ModifiedTs time.Time | |
145 } | |
146 | |
147 // PackageACLChange is a mutation to some package ACL. | |
148 type PackageACLChange struct { | |
149 // Action defines what action to perform: GrantRole or RevokeRole. | |
150 Action PackageACLChangeAction | |
151 // Role to grant or revoke to a user or group. | |
152 Role string | |
153 // Principal is a user or a group to grant or revoke a role for. | |
154 Principal string | |
155 } | |
156 | |
157 // UploadSession describes open CAS upload session. | |
158 type UploadSession struct { | |
159 // ID identifies upload session in the backend. | |
160 ID string | |
161 // URL is where to upload the data to. | |
162 URL string | |
163 } | |
164 | |
165 // NewClient initializes default CIPD client object. Its fields can be further | |
166 // tweaked after this call. | |
167 func NewClient() *Client { | |
168 c := &Client{ | |
169 ServiceURL: ProdServiceURL, | |
170 Log: logging.DefaultLogger, | |
171 AuthenticatedClientFactory: func() (*http.Client, error) { retur n http.DefaultClient, nil }, | |
172 AnonymousClientFactory: func() (*http.Client, error) { retur n http.DefaultClient, nil }, | |
173 UserAgent: UserAgent, | |
174 } | |
nodir
2015/05/12 19:09:20
Curious why clock is not here?
Vadim Sh.
2015/05/12 23:25:03
Just forgot
| |
175 if !build.ReleaseBuild { | |
176 c.ServiceURL = TestingServiceURL | |
177 c.UserAgent += " testing" | |
178 } | |
179 c.clock = &clockImpl{} | |
180 c.remote = &remoteImpl{c} | |
181 c.storage = &storageImpl{c, uploadChunkSize} | |
182 return c | |
183 } | |
184 | |
185 // doAuthenticatedHTTPRequest is used by remote implementation to make HTTP call s. | |
186 func (client *Client) doAuthenticatedHTTPRequest(req *http.Request) (*http.Respo nse, error) { | |
187 if client.authClient == nil { | |
188 var err error | |
189 client.authClient, err = client.AuthenticatedClientFactory() | |
190 if err != nil { | |
191 return nil, err | |
192 } | |
193 } | |
194 return client.authClient.Do(req) | |
195 } | |
196 | |
197 // doAnonymousHTTPRequest is used by storage implementation to make HTTP calls. | |
198 func (client *Client) doAnonymousHTTPRequest(req *http.Request) (*http.Response, error) { | |
199 if client.anonClient == nil { | |
200 var err error | |
201 client.anonClient, err = client.AnonymousClientFactory() | |
202 if err != nil { | |
203 return nil, err | |
204 } | |
205 } | |
206 return client.anonClient.Do(req) | |
207 } | |
nodir
2015/05/12 19:09:20
I wonder why here and in client factories, you hav
Vadim Sh.
2015/05/12 23:25:03
I think c.doAnonymousHTTPRequest(req) is more read
| |
208 | |
209 // FetchACL returns a list of PackageACL objects (parent paths first) that | |
210 // together define access control list for given package subpath. | |
nodir
2015/05/12 19:09:20
the access control list for the given package subp
Vadim Sh.
2015/05/12 23:25:02
Done.
| |
211 func (client *Client) FetchACL(packagePath string) ([]PackageACL, error) { | |
212 return client.remote.fetchACL(packagePath) | |
213 } | |
214 | |
215 // ModifyACL applies a set of PackageACLChanges to a package path. | |
216 func (client *Client) ModifyACL(packagePath string, changes []PackageACLChange) error { | |
217 return client.remote.modifyACL(packagePath, changes) | |
218 } | |
219 | |
220 // UploadToCAS uploads package data blob to Content Addressed Store if it is not | |
221 // there already. The data is addressed by SHA1 hash (also known as package's | |
222 // InstanceID). It can be used as a standalone function (if 'session' is nil) | |
223 // or as a part of more high level upload process (in that case upload session | |
224 // can be opened elsewhere and its properties passed here via 'session' | |
225 // argument). Returns nil on successful upload. | |
226 func (client *Client) UploadToCAS(SHA1 string, data io.ReadSeeker, session *Uplo adSession) error { | |
nodir
2015/05/12 19:09:20
why SHA1 is capital letters?
Vadim Sh.
2015/05/12 23:25:03
Done.
| |
227 // Open new upload session if existing is not provided. | |
nodir
2015/05/12 19:09:19
an existing
Vadim Sh.
2015/05/12 23:25:03
Done.
| |
228 var err error | |
229 if session == nil { | |
230 client.Log.Infof("cipd: uploading %s: initiating", SHA1) | |
231 session, err = client.remote.initiateUpload(SHA1) | |
232 if err != nil { | |
233 client.Log.Warningf("cipd: can't upload %s - %s", SHA1, err) | |
234 return err | |
235 } | |
236 if session == nil { | |
237 client.Log.Infof("cipd: %s is already uploaded", SHA1) | |
238 return nil | |
239 } | |
240 } else { | |
241 if session.ID == "" || session.URL == "" { | |
242 return ErrBadUploadSession | |
243 } | |
244 } | |
245 | |
246 // Upload the file to CAS storage. | |
247 err = client.storage.upload(session.URL, data) | |
248 if err != nil { | |
249 return err | |
250 } | |
251 | |
252 // Finalize the upload, wait until server verifies and publishes the fil e. | |
253 started := client.clock.now() | |
254 delay := time.Second | |
255 for { | |
256 published, err := client.remote.finalizeUpload(session.ID) | |
257 if published { | |
258 client.Log.Infof("cipd: successfully uploaded %s", SHA1) | |
259 return nil | |
260 } | |
261 if err != nil { | |
nodir
2015/05/12 19:09:19
put err check before published
Vadim Sh.
2015/05/12 23:25:02
Done.
| |
262 client.Log.Warningf("cipd: upload of %s failed: %s", SHA 1, err) | |
263 return err | |
264 } | |
265 if client.clock.now().Sub(started) > CASFinalizationTimeout { | |
266 client.Log.Warningf("cipd: upload of %s failed: timeout" , SHA1) | |
267 return ErrFinalizationTimeout | |
268 } | |
269 client.Log.Infof("cipd: uploading - verifying") | |
270 client.clock.sleep(delay) | |
271 if delay < 4*time.Second { | |
272 delay += 500 * time.Millisecond | |
273 } | |
274 } | |
275 } | |
276 | |
277 // RegisterInstance makes the package instance available for clients by | |
278 // uploading it to the storage and registering it in the package repository. | |
279 // 'instance' is a package instance to register. | |
280 func (client *Client) RegisterInstance(instance local.PackageInstance) error { | |
281 // Attempt to register. | |
282 client.Log.Infof("cipd: registering %s", instance.Pin()) | |
283 result, err := client.remote.registerInstance(instance.Pin()) | |
284 if err != nil { | |
285 return err | |
286 } | |
287 | |
288 // Asked to upload the package file to CAS first? | |
289 if result.uploadSession != nil { | |
290 err = client.UploadToCAS(instance.Pin().InstanceID, instance.Dat aReader(), result.uploadSession) | |
291 if err != nil { | |
292 return err | |
293 } | |
294 // Try again, now that file is uploaded. | |
295 client.Log.Infof("cipd: registering %s", instance.Pin()) | |
296 result, err = client.remote.registerInstance(instance.Pin()) | |
297 if err != nil { | |
298 return err | |
299 } | |
300 if result.uploadSession != nil { | |
301 return ErrBadUpload | |
302 } | |
303 } | |
304 | |
305 if result.alreadyRegistered { | |
306 client.Log.Infof( | |
307 "cipd: instance %s is already registered by %s on %s", | |
308 instance.Pin(), result.registeredBy, result.registeredTs ) | |
309 } else { | |
310 client.Log.Infof("cipd: instance %s was successfully registered" , instance.Pin()) | |
311 } | |
312 | |
313 return nil | |
314 } | |
315 | |
316 // AttachTagsWhenReady attaches tags to an instance retrying on "not yet process ed" responses. | |
317 func (client *Client) AttachTagsWhenReady(pin common.Pin, tags []string) error { | |
318 err := common.ValidatePin(pin) | |
319 if err != nil { | |
320 return err | |
321 } | |
322 if len(tags) == 0 { | |
323 return nil | |
324 } | |
325 for _, tag := range tags { | |
326 client.Log.Infof("cipd: attaching tag %s", tag) | |
327 } | |
328 deadline := client.clock.now().Add(TagAttachTimeout) | |
329 for client.clock.now().Before(deadline) { | |
330 err = client.remote.attachTags(pin, tags) | |
331 if err == nil { | |
332 client.Log.Infof("cipd: all tags attached") | |
333 return nil | |
334 } | |
335 if _, ok := err.(*pendingProcessingError); ok { | |
336 client.Log.Warningf("cipd: package instance is not ready yet - %s", err) | |
337 client.clock.sleep(5 * time.Second) | |
338 } else { | |
339 client.Log.Errorf("cipd: failed to attach tags - %s", er r) | |
340 return err | |
341 } | |
342 } | |
343 client.Log.Errorf("cipd: failed to attach tags - deadline exceeded") | |
344 return ErrAttachTagsTimeout | |
345 } | |
346 | |
347 // FetchInstance downloads package instance file from the repository. | |
348 func (client *Client) FetchInstance(pin common.Pin, output io.WriteSeeker) error { | |
349 err := common.ValidatePin(pin) | |
350 if err != nil { | |
351 return err | |
352 } | |
353 client.Log.Infof("cipd: resolving fetch URL for %s", pin) | |
354 fetchInfo, err := client.remote.fetchInstance(pin) | |
355 if err == nil { | |
356 err = client.storage.download(fetchInfo.fetchURL, output) | |
357 } | |
358 if err != nil { | |
359 client.Log.Errorf("cipd: failed to fetch %s (%s)", pin, err) | |
nodir
2015/05/12 19:09:20
normally use use " - " between summary and error d
Vadim Sh.
2015/05/12 23:25:03
Done.
| |
360 return err | |
361 } | |
362 client.Log.Infof("cipd: successfully fetched %s", pin) | |
363 return nil | |
364 } | |
365 | |
366 // FetchAndDeployInstance fetches the package instance and deploys it into | |
367 // a site root. It doesn't check whether the instance is already deployed. | |
368 func (client *Client) FetchAndDeployInstance(root string, pin common.Pin) error { | |
369 err := common.ValidatePin(pin) | |
370 if err != nil { | |
371 return err | |
372 } | |
373 | |
374 // Use temp file for storing package file. Delete it when done. | |
375 var instance local.PackageInstance | |
376 tempPath := filepath.Join(root, local.SiteServiceDir, "tmp") | |
377 err = os.MkdirAll(tempPath, 0777) | |
378 if err != nil { | |
379 return err | |
380 } | |
381 f, err := ioutil.TempFile(tempPath, pin.InstanceID) | |
382 if err != nil { | |
383 return err | |
384 } | |
385 defer func() { | |
386 // Instance takes ownership of the file, no need to close it sep arately. | |
387 if instance == nil { | |
388 f.Close() | |
389 } | |
390 os.Remove(f.Name()) | |
391 }() | |
392 | |
393 // Fetch the package data to the provided storage. | |
394 err = client.FetchInstance(pin, f) | |
395 if err != nil { | |
396 return err | |
397 } | |
398 | |
399 // Open the instance, verify the instance ID. | |
400 instance, err = local.OpenInstance(f, pin.InstanceID) | |
401 if err != nil { | |
402 return err | |
403 } | |
404 defer instance.Close() | |
405 | |
406 // Deploy it. 'defer' will take care of removing the temp file if needed . | |
407 _, err = local.DeployInstance(root, instance) | |
408 return err | |
409 } | |
410 | |
411 // ProcessEnsureFile parses text file that describes what should be installed | |
412 // by EnsurePackages function. It is a text file where each line has a form: | |
413 // <package name> <desired version>. Whitespaces are ignored. Lines that start | |
414 // with '#' are ignored. Version can be specified as instance ID, tag or ref. | |
415 // Will resolve tags and refs to concrete instance IDs. | |
416 func (client *Client) ProcessEnsureFile(r io.Reader) ([]common.Pin, error) { | |
417 lineNo := 0 | |
418 makeError := func(msg string) error { | |
419 return fmt.Errorf("Failed to parse desired state (line %d): %s", lineNo, msg) | |
420 } | |
421 | |
422 out := []common.Pin{} | |
423 scanner := bufio.NewScanner(r) | |
424 for scanner.Scan() { | |
425 lineNo++ | |
426 | |
427 // Split each line into words, ignore white space. | |
428 tokens := []string{} | |
429 for _, chunk := range strings.Split(scanner.Text(), " ") { | |
430 chunk = strings.TrimSpace(chunk) | |
431 if chunk != "" { | |
432 tokens = append(tokens, chunk) | |
433 } | |
434 } | |
435 | |
436 // Skip empty lines or lines starting with '#'. | |
437 if len(tokens) == 0 || tokens[0][0] == '#' { | |
438 continue | |
439 } | |
440 | |
441 // Each line has a format "<package name> <instance id>". | |
nodir
2015/05/12 19:09:19
version, not instance id
Vadim Sh.
2015/05/12 23:25:03
Done.
| |
442 if len(tokens) != 2 { | |
443 return nil, makeError("expecting '<package name> <instan ce id>' line") | |
nodir
2015/05/12 19:09:19
here too
Vadim Sh.
2015/05/12 23:25:03
Done.
| |
444 } | |
445 err := common.ValidatePackageName(tokens[0]) | |
446 if err != nil { | |
447 return nil, makeError(err.Error()) | |
448 } | |
449 err = common.ValidateInstanceID(tokens[1]) | |
nodir
2015/05/12 19:09:20
validate version?
Vadim Sh.
2015/05/12 23:25:02
See TODO, it is not implemented yet.
| |
450 if err != nil { | |
451 return nil, makeError(err.Error()) | |
452 } | |
453 | |
454 // Good enough. | |
455 out = append(out, common.Pin{PackageName: tokens[0], InstanceID: tokens[1]}) | |
456 } | |
457 | |
458 // TODO(vadimsh): Resolve tags to instance IDs. | |
459 return out, nil | |
460 } | |
461 | |
462 // EnsurePackages is high level interface for installation, removal and updates | |
nodir
2015/05/12 19:09:19
nit: a high-level
Vadim Sh.
2015/05/12 23:25:03
Done.
| |
463 // of packages inside some installation site root. Given a description of | |
464 // what packages (and versions) should be installed it will do all necessary | |
465 // actions to bring the state of the site root to desired one. | |
nodir
2015/05/12 19:09:19
the desired one
Vadim Sh.
2015/05/12 23:25:03
Done.
| |
466 func (client *Client) EnsurePackages(root string, pins []common.Pin) error { | |
467 // Make sure a package is specified only once. | |
468 seen := make(map[string]bool, len(pins)) | |
469 for _, p := range pins { | |
470 if seen[p.PackageName] { | |
471 return fmt.Errorf("Package %s is specified twice", p.Pac kageName) | |
472 } | |
473 seen[p.PackageName] = true | |
474 } | |
475 | |
476 // Ensure site root is a directory (or missing). | |
477 root, err := filepath.Abs(filepath.Clean(root)) | |
478 if err != nil { | |
479 return err | |
480 } | |
481 stat, err := os.Stat(root) | |
482 if err == nil && !stat.IsDir() { | |
483 return fmt.Errorf("Path %s is not a directory", root) | |
484 } | |
485 if err != nil && !os.IsNotExist(err) { | |
486 return err | |
487 } | |
488 rootExists := (err == nil) | |
489 | |
490 // Enumerate existing packages (only if root already exists). | |
491 existing := []common.Pin{} | |
492 if rootExists { | |
493 existing, err = local.FindDeployed(root) | |
494 if err != nil { | |
495 client.Log.Errorf("Failed to enumerate installed package s: %s", err) | |
496 return err | |
497 } | |
498 } | |
499 | |
500 // Figure out what needs to be updated and deleted, log it. | |
501 toDeploy, toDelete := buildActionPlan(pins, existing) | |
502 if len(toDeploy) == 0 && len(toDelete) == 0 { | |
503 client.Log.Infof("Everything is up-to-date.") | |
504 return nil | |
505 } | |
506 if len(toDeploy) != 0 { | |
507 client.Log.Infof("Packages to be installed:") | |
508 for _, pin := range toDeploy { | |
509 client.Log.Infof(" %s", pin) | |
510 } | |
511 } | |
512 if len(toDelete) != 0 { | |
513 client.Log.Infof("Packages to be removed:") | |
514 for _, pin := range toDelete { | |
515 client.Log.Infof(" %s", pin) | |
516 } | |
517 } | |
518 | |
519 // Create the site root directory before installing anything there. | |
520 if len(toDeploy) != 0 && !rootExists { | |
521 err = os.MkdirAll(root, 0777) | |
522 if err != nil { | |
523 return err | |
524 } | |
525 } | |
526 | |
527 // Remove all unneeded stuff. | |
528 errors := []error{} | |
529 for _, pin := range toDelete { | |
530 err = local.RemoveDeployed(root, pin.PackageName) | |
531 if err != nil { | |
532 client.Log.Errorf("Failed to remove %s - %s", pin.Packag eName, err) | |
533 errors = append(errors, err) | |
534 } | |
535 } | |
536 | |
537 // Install all new stuff. | |
538 for _, pin := range toDeploy { | |
539 err = client.FetchAndDeployInstance(root, pin) | |
540 if err != nil { | |
541 client.Log.Errorf("Failed to install %s - %s", pin, err) | |
542 errors = append(errors, err) | |
nodir
2015/05/12 19:09:20
Consider prepending pin info to err message before
| |
543 } | |
544 } | |
545 | |
546 if len(errors) == 0 { | |
547 client.Log.Infof("All changes applied.") | |
548 return nil | |
549 } | |
550 return fmt.Errorf("Some actions failed: %v", errors) | |
nodir
2015/05/12 19:09:20
report errors on individual lines, otherwise it wi
Vadim Sh.
2015/05/12 23:25:02
Are multiline error objects allowed? Replaced this
nodir
2015/05/12 23:35:53
This is fine. My concern was that %v with an array
| |
551 } | |
552 | |
553 //////////////////////////////////////////////////////////////////////////////// | |
554 // Private structs and interfaces. | |
555 | |
556 type clock interface { | |
557 now() time.Time | |
558 sleep(time.Duration) | |
559 } | |
560 | |
561 type remote interface { | |
562 fetchACL(packagePath string) ([]PackageACL, error) | |
563 modifyACL(packagePath string, changes []PackageACLChange) error | |
564 | |
565 initiateUpload(sha1 string) (*UploadSession, error) | |
566 finalizeUpload(sessionID string) (bool, error) | |
567 registerInstance(pin common.Pin) (*registerInstanceResponse, error) | |
568 | |
569 attachTags(pin common.Pin, tags []string) error | |
570 fetchInstance(pin common.Pin) (*fetchInstanceResponse, error) | |
571 } | |
572 | |
573 type storage interface { | |
574 upload(url string, data io.ReadSeeker) error | |
575 download(url string, output io.WriteSeeker) error | |
576 } | |
577 | |
578 type registerInstanceResponse struct { | |
579 uploadSession *UploadSession | |
580 alreadyRegistered bool | |
581 registeredBy string | |
582 registeredTs time.Time | |
583 } | |
584 | |
585 type fetchInstanceResponse struct { | |
586 fetchURL string | |
587 registeredBy string | |
588 registeredTs time.Time | |
589 } | |
590 | |
591 // Private stuff. | |
592 | |
593 type clockImpl struct{} | |
594 | |
595 func (c *clockImpl) now() time.Time { return time.Now() } | |
596 func (c *clockImpl) sleep(d time.Duration) { time.Sleep(d) } | |
597 | |
598 // buildActionPlan is used by EnsurePackages to figure out what to install or re move. | |
599 func buildActionPlan(desired []common.Pin, existing []common.Pin) (toDeploy []co mmon.Pin, toDelete []common.Pin) { | |
nodir
2015/05/12 19:09:19
toDeploy, toDelete []common.Pin
Vadim Sh.
2015/05/12 23:25:02
Done.
| |
600 // Figure out what needs to be installed or updated. | |
601 for _, d := range desired { | |
602 alreadyGood := false | |
603 for _, e := range existing { | |
604 if e.PackageName == d.PackageName { | |
605 alreadyGood = e.InstanceID == d.InstanceID | |
nodir
2015/05/12 19:09:20
build a map package name -> instanceId for `existi
Vadim Sh.
2015/05/12 23:25:03
Done because it makes code shorter. As optimizatio
nodir
2015/05/12 23:35:53
I expected it to be much more than 1-2
| |
606 break | |
607 } | |
608 } | |
609 if !alreadyGood { | |
610 toDeploy = append(toDeploy, d) | |
611 } | |
612 } | |
613 | |
614 // Figure out what needs to be removed. | |
615 for _, e := range existing { | |
616 keep := false | |
617 for _, d := range desired { | |
618 if e.PackageName == d.PackageName { | |
619 keep = true | |
620 break | |
621 } | |
622 } | |
623 if !keep { | |
624 toDelete = append(toDelete, e) | |
625 } | |
626 } | |
627 | |
628 return | |
629 } | |
OLD | NEW |