github.com/btwiuse/jiri@v0.0.0-20191125065820-53353bcfef54/cipd/cipd.go (about) 1 // Copyright 2018 The Fuchsia Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package cipd 6 7 import ( 8 "bufio" 9 "bytes" 10 "context" 11 "crypto/sha256" 12 "encoding/json" 13 "errors" 14 "fmt" 15 "io/ioutil" 16 "net/http" 17 "os" 18 "os/exec" 19 "path" 20 "path/filepath" 21 "regexp" 22 "runtime" 23 "strconv" 24 "strings" 25 "sync" 26 "time" 27 28 "github.com/btwiuse/jiri" 29 "github.com/btwiuse/jiri/log" 30 "github.com/btwiuse/jiri/version" 31 "golang.org/x/sync/semaphore" 32 ) 33 34 const ( 35 cipdBackend = "https://chrome-infra-packages.appspot.com" 36 // This git hash corresponds to a commit in https://chromium.googlesource.com/infra/infra 37 // to update the pinned version of the CIPD client in the DEPS file. 38 cipdVersion = "git_revision:0c0bef53cf8083d96ea39d24152fd0362b521ed7" 39 cipdVersionDigest = ` 40 # This file was generated by 41 # 42 # cipd selfupdate-roll -version-file .cipd_version \ 43 # -version git_revision:0c0bef53cf8083d96ea39d24152fd0362b521ed7 44 # 45 # Do not modify manually. All changes will be overwritten. 46 # Use 'cipd selfupdate-roll ...' to modify. 47 48 linux-386 sha256 4958b7a7623231c6b1a530da5bc88fb1468e7fa165ddb8802cc14ad842de8212 49 linux-amd64 sha256 292213783cd4302e26a501085adda272ccbd7d4733a865bbeb3cb050f3b34194 50 linux-arm64 sha256 759f5a7c1433f6d99c99c4156e3997c3926c2435c960212c6e324ab6bd64ad01 51 linux-armv6l sha256 b4bcdfaa45c2d7047ced6594368758dc624bf718babfe14851e6de7a994f24b8 52 linux-mips64 sha256 62fd72cbe4131d820fda543458007fbeca81a9234c86a3ae6910255ca17a679f 53 linux-mips64le sha256 2c8cda73279b00fc46cf55b09b3db984f5a8222753b87e3ed3ee280c807fb942 54 linux-mipsle sha256 6d0b68a8f013c7ee1c4f4980a788aaddf3bbc4cfecdd811e6e0df9b481a0d064 55 linux-ppc64 sha256 d3b6146d37d2aec0514ac67151aed61eb995a1015fee7026fc0cac415d1282ff 56 linux-ppc64le sha256 fcb0c726dc2c37308b2ece48a095027dcbfb86e65bc677a8b1b3fd18d20dad23 57 linux-s390x sha256 2f4afbea4b1f5d7c4c474882de661bd35e6fdb4ecd34533a05ead8b48a3602ea 58 mac-amd64 sha256 7a8105e4e2bc95f2cc3060dcffb44c53391ff7e380f89be276102f619910ae17 59 windows-386 sha256 59c6d695a24973ef42e731e86fb055827df4f3e060481605e36c6e2d8dfd6ff0 60 windows-amd64 sha256 1d26180a78ac11c5a940e7f7a26cdf483cf81ccf0e98e29007932a2eb7d621e0 61 ` 62 cipdNotLoggedInStr = "Not logged in" 63 ) 64 65 var ( 66 // CipdPlatform represents the current runtime platform in cipd platform notation. 67 CipdPlatform Platform 68 cipdOS string 69 cipdArch string 70 cipdBinary string 71 selfUpdateOnce sync.Once 72 templateRE = regexp.MustCompile(`\${[^}]*}`) 73 74 // ErrSkipTemplate may be returned from Expander.Expand to indicate that 75 // a given expansion doesn't apply to the current template parameters. For 76 // example, expanding `"foo/${os=linux,mac}"` with a template parameter of "os" 77 // == "win", would return ErrSkipTemplate. 78 ErrSkipTemplate = errors.New("package template does not apply to the current system") 79 ) 80 81 func init() { 82 cipdOS = runtime.GOOS 83 cipdArch = runtime.GOARCH 84 if cipdOS == "darwin" { 85 cipdOS = "mac" 86 } 87 if cipdArch == "arm" { 88 cipdArch = "armv6l" 89 } 90 CipdPlatform = Platform{cipdOS, cipdArch} 91 } 92 93 func fetchBinary(binaryPath, platform, version, digest string) error { 94 cipdURL := fmt.Sprintf("%s/client?platform=%s&version=%s", cipdBackend, platform, version) 95 data, err := fetchFile(cipdURL) 96 if err != nil { 97 return err 98 } 99 if verified, err := verifyDigest(data, digest); err != nil || !verified { 100 if err != nil { 101 return err 102 } 103 return errors.New("cipd failed integrity test") 104 } 105 // cipd binary verified. Save to disk 106 if _, err := os.Stat(filepath.Dir(binaryPath)); os.IsNotExist(err) { 107 if err := os.MkdirAll(filepath.Dir(binaryPath), 0755); err != nil { 108 return fmt.Errorf("failed to create parent directory %q for cipd: %v", filepath.Dir(binaryPath), err) 109 } 110 } 111 return writeFile(binaryPath, data) 112 } 113 114 // Bootstrap returns the path of a valid cipd binary. It will fetch cipd from 115 // remote if a valid cipd binary is not found. It will update cipd if there 116 // is a new version. 117 func Bootstrap(binaryPath string) (string, error) { 118 cipdBinary = binaryPath 119 bootstrap := func() error { 120 // Fetch cipd digest 121 cipdDigest, _, err := fetchDigest(CipdPlatform.String()) 122 if err != nil { 123 return err 124 } 125 if cipdBinary == "" { 126 return errors.New("cipd binary path was not set") 127 } 128 if err != nil { 129 return err 130 } 131 return fetchBinary(cipdBinary, CipdPlatform.String(), cipdVersion, cipdDigest) 132 } 133 134 getCipd := func() (string, error) { 135 if cipdBinary == "" { 136 return "", errors.New("cipd binary path was not set") 137 } 138 fileInfo, err := os.Stat(cipdBinary) 139 if err != nil { 140 if os.IsNotExist(err) { 141 return "", fmt.Errorf("cipd binary was not found at %q", cipdBinary) 142 } 143 return "", err 144 } 145 // Check if cipd binary has execution permission 146 if fileInfo.Mode()&0111 == 0 { 147 return "", fmt.Errorf("cipd binary at %q is not executable", cipdBinary) 148 } 149 return cipdBinary, nil 150 } 151 152 cipdPath, err := getCipd() 153 if err != nil { 154 // Could not find cipd binary or cipd is invalid 155 // Bootstrap it from scratch 156 if err := bootstrap(); err != nil { 157 return "", err 158 } 159 return cipdBinary, nil 160 } 161 // cipd is found, do self update 162 var e error 163 selfUpdateOnce.Do(func() { 164 e = selfUpdate(cipdPath, cipdVersion) 165 }) 166 if e != nil { 167 // Self update is unsuccessful, redo bootstrap 168 if err := bootstrap(); err != nil { 169 return "", err 170 } 171 } 172 return cipdPath, nil 173 } 174 175 // FuchsiaPlatform returns a Platform struct which can be used in 176 // determing the correct path for prebuilt packages. It replace 177 // the os and arch names from cipd format to a format used by 178 // Fuchsia developers. 179 func FuchsiaPlatform(plat Platform) Platform { 180 retPlat := Platform{ 181 OS: plat.OS, 182 Arch: plat.Arch, 183 } 184 // Currently cipd use "amd64" for x86_64 while fuchsia use "x64", 185 // replace "amd64" with "x64". 186 // There might be other differences that need to be addressed in 187 // the future. 188 switch retPlat.Arch { 189 case "amd64": 190 retPlat.Arch = "x64" 191 } 192 return retPlat 193 } 194 195 func fetchDigest(platform string) (digest, method string, err error) { 196 var digestBuf bytes.Buffer 197 digestBuf.Write([]byte(cipdVersionDigest)) 198 digestScanner := bufio.NewScanner(&digestBuf) 199 for digestScanner.Scan() { 200 curLine := digestScanner.Text() 201 if len(curLine) == 0 || curLine[0] == '#' { 202 // Skip comment or empty line 203 continue 204 } 205 fields := strings.Fields(curLine) 206 if len(fields) != 3 { 207 return "", "", errors.New("unsupported cipd digest file format") 208 } 209 if fields[0] == platform { 210 digest = fields[2] 211 method = fields[1] 212 err = nil 213 return 214 } 215 } 216 return "", "", errors.New("no matching platform found in cipd digest file") 217 } 218 219 func selfUpdate(cipdPath, cipdVersion string) error { 220 args := []string{"selfupdate", "-version", cipdVersion, "-service-url", cipdBackend} 221 command := exec.Command(cipdPath, args...) 222 return command.Run() 223 } 224 225 func writeFile(filePath string, data []byte) error { 226 tempFile, err := ioutil.TempFile(path.Dir(filePath), "cipd.*") 227 if err != nil { 228 return err 229 } 230 defer tempFile.Close() 231 defer os.Remove(tempFile.Name()) 232 if _, err := tempFile.Write(data); err != nil { 233 // Write errors 234 return errors.New("I/O error while downloading cipd binary") 235 } 236 // Set mode to rwxr-xr-x 237 if err := tempFile.Chmod(0755); err != nil { 238 // Chmod errors 239 return errors.New("I/O error while adding executable permission to cipd binary") 240 } 241 tempFile.Close() 242 if err := os.Rename(tempFile.Name(), filePath); err != nil { 243 return err 244 } 245 return nil 246 } 247 248 func verifyDigest(data []byte, cipdDigest string) (bool, error) { 249 hash := sha256.Sum256(data) 250 hashString := fmt.Sprintf("%x", hash) 251 if hashString == strings.ToLower(cipdDigest) { 252 return true, nil 253 } 254 return false, nil 255 } 256 257 func getUserAgent() string { 258 ua := "jiri/" + version.GitCommit 259 if version.GitCommit == "" { 260 ua += "debug" 261 } 262 return ua 263 } 264 265 func fetchFile(url string) ([]byte, error) { 266 client := &http.Client{} 267 req, err := http.NewRequest("GET", url, nil) 268 if err != nil { 269 return nil, err 270 } 271 req.Header.Set("User-Agent", getUserAgent()) 272 resp, err := client.Do(req) 273 if err != nil { 274 return nil, err 275 } 276 defer resp.Body.Close() 277 return ioutil.ReadAll(resp.Body) 278 } 279 280 type packageACL struct { 281 path string 282 access bool 283 } 284 285 func checkPackageACL(jirix *jiri.X, cipdPath, jsonDir string, c chan<- packageACL) { 286 // cipd should be already bootstrapped before this go routine. 287 // Silently return a false just in case if cipd is not found. 288 if cipdBinary == "" { 289 c <- packageACL{path: cipdPath, access: false} 290 return 291 } 292 293 jsonFile, err := ioutil.TempFile(jsonDir, "cipd*.json") 294 if err != nil { 295 jirix.Logger.Warningf("Error while creating temporary file for cipd") 296 c <- packageACL{path: cipdPath, access: false} 297 return 298 } 299 jsonFileName := jsonFile.Name() 300 jsonFile.Close() 301 302 args := []string{"acl-check", "-reader", "-json-output", jsonFileName, cipdPath} 303 jirix.Logger.Debugf("Invoke cipd with %v", args) 304 305 command := exec.Command(cipdBinary, args...) 306 var stdoutBuf, stderrBuf bytes.Buffer 307 command.Stdout = &stdoutBuf 308 command.Stderr = &stderrBuf 309 // Return false if cipd cannot be executed or output jsonfile contains false. 310 if err := command.Run(); err != nil { 311 jirix.Logger.Debugf("Error happend while executing cipd, err: %q, stderr: %q", err, stderrBuf.String()) 312 c <- packageACL{path: cipdPath, access: false} 313 return 314 } 315 316 jsonData, err := ioutil.ReadFile(jsonFileName) 317 if err != nil { 318 c <- packageACL{path: cipdPath, access: false} 319 return 320 } 321 322 var result struct { 323 Result bool `json:"result"` 324 } 325 if err := json.Unmarshal(jsonData, &result); err != nil { 326 c <- packageACL{path: cipdPath, access: false} 327 return 328 } 329 330 if !result.Result { 331 c <- packageACL{path: cipdPath, access: false} 332 return 333 } 334 335 // Package can be accessed. 336 c <- packageACL{path: cipdPath, access: true} 337 return 338 } 339 340 // CheckPackageACL checks cipd's access to packages in map "pkgs". The package 341 // names in "pkgs" should have trailing '/' removed before calling this 342 // function. 343 func CheckPackageACL(jirix *jiri.X, pkgs map[string]bool) error { 344 // Not declared as CheckPackageACL(jirix *jiri.X, pkgs map[*package.Package]bool) 345 // due to import cycles. Package jiri/package imports jiri/cipd so here we cannot 346 // import jiri/package. 347 if _, err := Bootstrap(jirix.CIPDPath()); err != nil { 348 return err 349 } 350 351 jsonDir, err := ioutil.TempDir("", "jiri_cipd") 352 if err != nil { 353 return err 354 } 355 defer os.RemoveAll(jsonDir) 356 357 c := make(chan packageACL) 358 for key := range pkgs { 359 go checkPackageACL(jirix, key, jsonDir, c) 360 } 361 362 for i := 0; i < len(pkgs); i++ { 363 acl := <-c 364 pkgs[acl.path] = acl.access 365 } 366 return nil 367 } 368 369 // CheckLoggedIn checks cipd's user login information. It will return true 370 // if login information is found or return false if login information is not 371 // found. 372 func CheckLoggedIn(jirix *jiri.X) (bool, error) { 373 cipdPath, err := Bootstrap(jirix.CIPDPath()) 374 if err != nil { 375 return false, err 376 } 377 args := []string{"auth-info"} 378 command := exec.Command(cipdPath, args...) 379 var stdoutBuf, stderrBuf bytes.Buffer 380 command.Stdout = &stdoutBuf 381 command.Stderr = &stderrBuf 382 if err := command.Run(); err != nil { 383 stdErrMsg := strings.TrimSpace(stderrBuf.String()) 384 jirix.Logger.Debugf("Error happend while executing cipd, err: %q, stderr: %q", err, stdErrMsg) 385 if _, ok := err.(*exec.ExitError); ok && stdErrMsg == cipdNotLoggedInStr { 386 return false, nil 387 } 388 return false, err 389 } 390 return true, nil 391 } 392 393 // Ensure runs cipd binary's ensure functionality over file. Fetched packages will be 394 // saved to projectRoot directory. Parameter timeout is in minutes. 395 func Ensure(jirix *jiri.X, file, projectRoot string, timeout uint) error { 396 cipdPath, err := Bootstrap(jirix.CIPDPath()) 397 if err != nil { 398 return err 399 } 400 ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Minute) 401 defer cancel() 402 args := []string{ 403 "ensure", 404 "-ensure-file", file, 405 "-root", projectRoot, 406 "-max-threads", strconv.Itoa(jirix.CipdMaxThreads), 407 } 408 409 // If jiri is *not* running with -v, use the less verbose cipd "warning" 410 // log-level. 411 if jirix.Logger.LoggerLevel < log.DebugLevel { 412 args = append(args, "-log-level", "warning") 413 } 414 415 task := jirix.Logger.AddTaskMsg("Fetching CIPD packages") 416 defer task.Done() 417 jirix.Logger.Debugf("Invoke cipd with %v", args) 418 419 // Construct arguments and invoke cipd for ensure file 420 command := exec.CommandContext(ctx, cipdPath, args...) 421 // Add User-Agent info for cipd 422 command.Env = append(os.Environ(), "CIPD_HTTP_USER_AGENT_PREFIX="+getUserAgent()) 423 command.Stdin = os.Stdin 424 command.Stdout = os.Stdout 425 command.Stderr = os.Stderr 426 427 err = command.Run() 428 if ctx.Err() == context.DeadlineExceeded { 429 err = ctx.Err() 430 } 431 return err 432 } 433 434 // TODO: Using PackageLock in project package directly will cause an import 435 // cycle. Remove this type once we solve the this issue. 436 437 // PackageInstance describes package instance id information generated by cipd 438 // ensure-file-resolve. It is a copy of PackageLock type in project package. 439 type PackageInstance struct { 440 PackageName string 441 VersionTag string 442 InstanceID string 443 } 444 445 // Resolve runs cipd binary's ensure-file-resolve functionality over file. 446 // It returns a slice containing resolved packages and cipd instance ids. 447 func Resolve(jirix *jiri.X, file string) ([]PackageInstance, error) { 448 cipdPath, err := Bootstrap(jirix.CIPDPath()) 449 if err != nil { 450 return nil, err 451 } 452 args := []string{"ensure-file-resolve", "-ensure-file", file, "-log-level", "warning"} 453 jirix.Logger.Debugf("Invoke cipd with %v", args) 454 455 command := exec.Command(cipdPath, args...) 456 command.Env = append(os.Environ(), "CIPD_HTTP_USER_AGENT_PREFIX="+getUserAgent()) 457 var stdoutBuf, stderrBuf bytes.Buffer 458 command.Stdin = os.Stdin 459 // Redirect outputs since cipd will print verbose information even 460 // if log-level is set to warning 461 command.Stdout = &stdoutBuf 462 command.Stderr = &stderrBuf 463 if err := command.Run(); err != nil { 464 jirix.Logger.Errorf("cipd returned error: %v", stderrBuf.String()) 465 return nil, err 466 } 467 468 // cipd generates the version file in the same directory of the ensure file 469 // if no error is returned 470 versionFile := file[:len(file)-len(".ensure")] + ".version" 471 defer os.Remove(versionFile) 472 return parseVersions(versionFile) 473 } 474 475 func parseVersions(file string) ([]PackageInstance, error) { 476 versionReader, err := os.Open(file) 477 if err != nil { 478 return nil, err 479 } 480 defer versionReader.Close() 481 versionScanner := bufio.NewScanner(versionReader) 482 // An example cipd version looks like: 483 // ========================================================== 484 // # Do not modify manually. All changes will be overwritten. 485 // fuchsia/clang/linux-amd64 486 // git_revision:280fa3c2d2ddb0b5dcb31113c0b1c2259982b7e7 487 // eRoGS8qgx370QAIRgLDmbhpdPey8ti47B2Z3LMzwcXQC 488 // 489 // fuchsia/clang/mac-amd64 490 // git_revision:280fa3c2d2ddb0b5dcb31113c0b1c2259982b7e7 491 // BQhlnpoWG081CyLzA0zB1vCr8YPdb2DO2jnYe3Lsw4oC 492 // =========================================================== 493 // Parse version file using DFA 494 495 const ( 496 stWaitingPkg = "a package name" 497 stWaitingVer = "a package version" 498 stWaitingIID = "an instance ID" 499 stWaitingNL = "a new line" 500 ) 501 502 state := stWaitingPkg 503 pkg := "" 504 ver := "" 505 iid := "" 506 lineNo := 0 507 makeError := func(fmtStr string, args ...interface{}) error { 508 args = append([]interface{}{lineNo}, args...) 509 return fmt.Errorf("failed to parse versions file (line %d): "+fmtStr, args...) 510 } 511 output := make([]PackageInstance, 0) 512 for versionScanner.Scan() { 513 lineNo++ 514 line := strings.TrimSpace(versionScanner.Text()) 515 // Comments are grammatically insignificant (unlike empty lines), so skip 516 // the completely. 517 if len(line) > 0 && line[0] == '#' { 518 continue 519 } 520 521 switch state { 522 case stWaitingPkg: 523 if line == "" { 524 continue // can have more than one empty line between triples 525 } 526 pkg = line 527 state = stWaitingVer 528 529 case stWaitingVer: 530 if line == "" { 531 return nil, makeError("expecting a version name, not a new line") 532 } 533 ver = line 534 state = stWaitingIID 535 536 case stWaitingIID: 537 if line == "" { 538 return nil, makeError("expecting an instance ID, not a new line") 539 } 540 iid = line 541 output = append(output, PackageInstance{pkg, ver, iid}) 542 pkg, ver, iid = "", "", "" 543 state = stWaitingNL 544 545 case stWaitingNL: 546 if line == "" { 547 state = stWaitingPkg 548 continue 549 } 550 return nil, makeError("expecting an empty line between each version definition triple") 551 } 552 } 553 return output, nil 554 } 555 556 type packageFloatingRef struct { 557 pkg PackageInstance 558 err error 559 floating bool 560 } 561 562 // CheckFloatingRefs determines if pkgs contains a floating ref which shouldn't 563 // be used normally. 564 func CheckFloatingRefs(jirix *jiri.X, pkgs map[PackageInstance]bool, plats map[PackageInstance][]Platform) error { 565 if _, err := Bootstrap(jirix.CIPDPath()); err != nil { 566 return err 567 } 568 569 jsonDir, err := ioutil.TempDir("", "jiri_cipd") 570 if err != nil { 571 return err 572 } 573 defer os.RemoveAll(jsonDir) 574 575 c := make(chan packageFloatingRef) 576 sem := semaphore.NewWeighted(10) 577 var errBuf bytes.Buffer 578 for k := range pkgs { 579 plat, ok := plats[k] 580 if !ok { 581 return fmt.Errorf("Platforms for package \"%s\" is not found", k.PackageName) 582 } 583 go checkFloatingRefs(jirix, k, plat, jsonDir, sem, c) 584 } 585 586 for i := 0; i < len(pkgs); i++ { 587 floatingRef := <-c 588 pkgs[floatingRef.pkg] = floatingRef.floating 589 if floatingRef.err != nil { 590 errBuf.WriteString(fmt.Sprintf("error happened while checking package %q with version %q: %v\n", floatingRef.pkg.PackageName, floatingRef.pkg.VersionTag, floatingRef.err.Error())) 591 } 592 } 593 594 if errBuf.Len() != 0 { 595 // Remote trailing '\n' 596 errBuf.Truncate(errBuf.Len() - 1) 597 return errors.New(errBuf.String()) 598 } 599 return nil 600 } 601 602 type describeJSON struct { 603 Refs []refsJSON `json:"refs,omitempty"` 604 } 605 606 type refsJSON struct { 607 Ref string `json:"ref,omitempty"` 608 } 609 610 func checkFloatingRefs(jirix *jiri.X, pkg PackageInstance, plats []Platform, jsonDir string, sem *semaphore.Weighted, c chan<- packageFloatingRef) { 611 // cipd should already bootstrapped before calling 612 // this function. 613 sem.Acquire(context.Background(), 1) 614 defer sem.Release(1) 615 if cipdBinary == "" { 616 c <- packageFloatingRef{ 617 pkg: pkg, 618 err: errors.New("cipd is not bootstrapped when calling checkFloatingRefs"), 619 floating: false, 620 } 621 return 622 } 623 // jsonFile will be cleaned up by caller. 624 jsonFile, err := ioutil.TempFile(jsonDir, "cipd*.json") 625 if err != nil { 626 c <- packageFloatingRef{ 627 pkg: pkg, 628 err: err, 629 floating: false, 630 } 631 return 632 } 633 jsonFileName := jsonFile.Name() 634 jsonFile.Close() 635 636 // Remove ${platform}, ${os} ... from package name before calling cipd describe 637 // as it will fail when these tags are not compatible with current host. 638 pkgName := pkg.PackageName 639 if MustExpand(pkgName) { 640 expandedPkgName, err := Expand(pkgName, plats) 641 if err != nil { 642 c <- packageFloatingRef{ 643 pkg: pkg, 644 err: err, 645 floating: false, 646 } 647 return 648 } 649 if len(expandedPkgName) == 0 { 650 c <- packageFloatingRef{ 651 pkg: pkg, 652 // avoid using %q as we don't want escape characters in the output. 653 err: fmt.Errorf("cannot expand package \"%s\"", pkgName), 654 floating: false, 655 } 656 return 657 } 658 pkgName = expandedPkgName[0] 659 } 660 661 args := []string{"describe", pkgName, "-version", pkg.VersionTag, "-json-output", jsonFileName} 662 jirix.Logger.Debugf("Invoke cipd with %v", args) 663 664 var stdoutBuf bytes.Buffer 665 var stderrBuf bytes.Buffer 666 command := exec.Command(cipdBinary, args...) 667 command.Env = append(os.Environ(), "CIPD_HTTP_USER_AGENT_PREFIX="+getUserAgent()) 668 command.Stdin = os.Stdin 669 command.Stdout = &stdoutBuf 670 command.Stderr = &stderrBuf 671 672 if err := command.Run(); err != nil { 673 c <- packageFloatingRef{ 674 pkg: pkg, 675 err: fmt.Errorf("cipd describe failed due to error: %v, stdout: %s\n, stderr: %s", err, stdoutBuf.String(), stderrBuf.String()), 676 floating: false, 677 } 678 return 679 } 680 681 jsonData, err := ioutil.ReadFile(jsonFileName) 682 if err != nil { 683 c <- packageFloatingRef{ 684 pkg: pkg, 685 err: err, 686 floating: false, 687 } 688 return 689 } 690 // Example of generated JSON: 691 // { 692 // "result": { 693 // "pin": { 694 // "package": "gn/gn/linux-amd64", 695 // "instance_id": "4usiirrra6WbnCKgplRoiJ8EcAsCuqCOd_7tpf_yXrAC" 696 // }, 697 // "registered_by": "user:infra-internal-gn-builder@chops-service-accounts.iam.gserviceaccount.com", 698 // "registered_ts": 1554328925, 699 // "refs": [ 700 // { 701 // "ref": "latest", 702 // "instance_id": "4usiirrra6WbnCKgplRoiJ8EcAsCuqCOd_7tpf_yXrAC", 703 // "modified_by": "user:infra-internal-gn-builder@chops-service-accounts.iam.gserviceaccount.com", 704 // "modified_ts": 1554328926 705 // } 706 // ], 707 // "tags": [ 708 // { 709 // "tag": "git_repository:https://gn.googlesource.com/gn", 710 // "registered_by": "user:infra-internal-gn-builder@chops-service-accounts.iam.gserviceaccount.com", 711 // "registered_ts": 1554328925 712 // }, 713 // { 714 // "tag": "git_revision:64b846c96daeb3eaf08e26d8a84d8451c6cb712b", 715 // "registered_by": "user:infra-internal-gn-builder@chops-service-accounts.iam.gserviceaccount.com", 716 // "registered_ts": 1554328925 717 // } 718 // ] 719 // } 720 // } 721 // Only "refs" is needed. 722 723 var result struct { 724 Result describeJSON `json:"result"` 725 } 726 727 if err := json.Unmarshal(jsonData, &result); err != nil { 728 c <- packageFloatingRef{ 729 pkg: pkg, 730 err: err, 731 floating: false, 732 } 733 return 734 } 735 736 for _, v := range result.Result.Refs { 737 if v.Ref == pkg.VersionTag { 738 c <- packageFloatingRef{pkg: pkg, err: nil, floating: true} 739 return 740 } 741 } 742 c <- packageFloatingRef{pkg: pkg, err: nil, floating: false} 743 return 744 } 745 746 // Platform contains the parameters for a "${platform}" template. 747 // The string value can be obtained by calling String(). 748 type Platform struct { 749 // OS defines the operating system of this platform. It can be any OS 750 // supported by golang. 751 OS string 752 // Arch defines the CPU architecture of this platform. It can be any 753 // architecture supported by golang. 754 Arch string 755 } 756 757 // NewPlatform parses a platform string into Platform struct. 758 func NewPlatform(s string) (Platform, error) { 759 fields := strings.Split(s, "-") 760 if len(fields) != 2 { 761 return Platform{"", ""}, fmt.Errorf("illegal platform %q", s) 762 } 763 return Platform{fields[0], fields[1]}, nil 764 } 765 766 // String generates a string represents the Platform in "OS"-"Arch" form. 767 func (p Platform) String() string { 768 return p.OS + "-" + p.Arch 769 } 770 771 // Expander returns an Expander populated with p's fields. 772 func (p Platform) Expander() Expander { 773 return Expander{ 774 "os": p.OS, 775 "arch": p.Arch, 776 "platform": p.String(), 777 } 778 } 779 780 // Expander is a mapping of simple string substitutions which is used to 781 // expand cipd package name templates. For example: 782 // 783 // ex, err := template.Expander{ 784 // "platform": "mac-amd64" 785 // }.Expand("foo/${platform}") 786 // 787 // `ex` would be "foo/mac-amd64". 788 type Expander map[string]string 789 790 // Expand applies package template expansion rules to the package template, 791 // 792 // If err == ErrSkipTemplate, that means that this template does not apply to 793 // this os/arch combination and should be skipped. 794 // 795 // The expansion rules are as follows: 796 // - "some text" will pass through unchanged 797 // - "${variable}" will directly substitute the given variable 798 // - "${variable=val1,val2}" will substitute the given variable, if its value 799 // matches one of the values in the list of values. If the current value 800 // does not match, this returns ErrSkipTemplate. 801 // 802 // Attempting to expand an unknown variable is an error. 803 // After expansion, any lingering '$' in the template is an error. 804 func (t Expander) Expand(template string) (pkg string, err error) { 805 skip := false 806 807 pkg = templateRE.ReplaceAllStringFunc(template, func(parm string) string { 808 // ${...} 809 contents := parm[2 : len(parm)-1] 810 811 varNameValues := strings.SplitN(contents, "=", 2) 812 if len(varNameValues) == 1 { 813 // ${varName} 814 if value, ok := t[varNameValues[0]]; ok { 815 return value 816 } 817 818 err = fmt.Errorf("unknown variable in ${%s}", contents) 819 } 820 821 // ${varName=value,value} 822 ourValue, ok := t[varNameValues[0]] 823 if !ok { 824 err = fmt.Errorf("unknown variable %q", parm) 825 return parm 826 } 827 828 for _, val := range strings.Split(varNameValues[1], ",") { 829 if val == ourValue { 830 return ourValue 831 } 832 } 833 skip = true 834 return parm 835 }) 836 if skip { 837 err = ErrSkipTemplate 838 } 839 if err == nil && strings.ContainsRune(pkg, '$') { 840 err = fmt.Errorf("unable to process some variables in %q", template) 841 } 842 return 843 } 844 845 // Expand method expands a cipdPath that contains templates such as ${platform} 846 // into concrete full paths. It might return an empty slice if platforms 847 // do not match the requirements in cipdPath. 848 func Expand(cipdPath string, platforms []Platform) ([]string, error) { 849 output := make([]string, 0) 850 //expanders := make([]Expander, 0) 851 if !MustExpand(cipdPath) { 852 output = append(output, cipdPath) 853 return output, nil 854 } 855 856 for _, plat := range platforms { 857 pkg, err := plat.Expander().Expand(cipdPath) 858 if err == ErrSkipTemplate { 859 continue 860 } 861 if err != nil { 862 return nil, err 863 } 864 output = append(output, pkg) 865 } 866 return output, nil 867 } 868 869 // Decl method expands a cipdPath that contains ${platform}, ${os}, ${arch} 870 // with information in platforms. Unlike the Expand method which 871 // returns a list of expanded cipd paths, the Decl method only returns a 872 // single path containing all platforms. For example, if platforms contain 873 // "linux-amd64" and "linux-arm64", ${platform} will be replaced to 874 // ${platform=linux-amd64,linux-arm64}. This is a workaround for a limitation 875 // in 'cipd ensure-file-resolve' which requires the header of '.ensure' file 876 // to contain all available platforms. But in some cases, a package may miss 877 // a particular platform, which will cause a crash on this cipd command. By 878 // explicitly list all supporting platforms in the cipdPath, we can avoid 879 // crashing cipd. 880 func Decl(cipdPath string, platforms []Platform) (string, error) { 881 if !MustExpand(cipdPath) || len(platforms) == 0 { 882 return cipdPath, nil 883 } 884 885 osMap := make(map[string]bool) 886 platMap := make(map[string]bool) 887 archMap := make(map[string]bool) 888 889 replacedOS := "${os=" 890 replacedArch := "${arch=" 891 replacedPlat := "${platform=" 892 893 for _, plat := range platforms { 894 if _, ok := osMap[plat.OS]; !ok { 895 replacedOS += plat.OS + "," 896 osMap[plat.OS] = true 897 } 898 if _, ok := archMap[plat.Arch]; !ok { 899 replacedArch += plat.Arch + "," 900 archMap[plat.Arch] = true 901 } 902 if _, ok := platMap[plat.String()]; !ok { 903 replacedPlat += plat.String() + "," 904 platMap[plat.String()] = true 905 } 906 } 907 replacedOS = replacedOS[:len(replacedOS)-1] + "}" 908 replacedArch = replacedArch[:len(replacedArch)-1] + "}" 909 replacedPlat = replacedPlat[:len(replacedPlat)-1] + "}" 910 911 cipdPath = strings.Replace(cipdPath, "${os}", replacedOS, -1) 912 cipdPath = strings.Replace(cipdPath, "${arch}", replacedArch, -1) 913 cipdPath = strings.Replace(cipdPath, "${platform}", replacedPlat, -1) 914 return cipdPath, nil 915 } 916 917 // MustExpand checks if template usages such as "${platform}" exist 918 // in cipdPath. If they exist, this function will return true. Otherwise 919 // it returns false. 920 func MustExpand(cipdPath string) bool { 921 return templateRE.MatchString(cipdPath) 922 } 923 924 // DefaultPlatforms returns a slice of Platform objects that are currently 925 // validated by jiri. 926 func DefaultPlatforms() []Platform { 927 return []Platform{ 928 Platform{"linux", "amd64"}, 929 Platform{"mac", "amd64"}, 930 } 931 }