Chromium Code Reviews| 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 |