[cipd] Add cipd bootstrap to jiri
This patch add cipd binary bootstrap feature to jiri with unit
tests. Jiri will only perform cipd bootstrap if there is a
<package> tag found in manifest files. Cipd will be saved to the
same directory that contains Jiri executable file.
BLD-201
Change-Id: Ic82aa2784b47c14a51b939a3851a1c7a232d34e9
diff --git a/cipd/cipd.go b/cipd/cipd.go
index 3fa6743..f34fa95 100644
--- a/cipd/cipd.go
+++ b/cipd/cipd.go
@@ -5,52 +5,264 @@
package cipd
import (
+ "bufio"
+ "bytes"
"context"
+ "crypto/sha256"
+ "errors"
"fmt"
+ "io/ioutil"
+ "net/http"
"os"
"os/exec"
"path"
+ "runtime"
+ "strings"
+ "sync"
"time"
"fuchsia.googlesource.com/jiri"
"fuchsia.googlesource.com/jiri/osutil"
+ "fuchsia.googlesource.com/jiri/version"
)
-func getCipdPath() (string, error) {
+const (
+ cipdBackend = "https://chrome-infra-packages.appspot.com"
+ cipdVersion = "git_revision:00e2d8b49a4e7505d1c71f19d15c9e7c5b9245a5"
+ cipdVersionDigest = `
+# This file was generated by
+#
+# cipd selfupdate-roll -version-file .cipd_version \
+# -version git_revision:00e2d8b49a4e7505d1c71f19d15c9e7c5b9245a5
+#
+# Do not modify manually. All changes will be overwritten.
+# Use 'cipd selfupdate-roll ...' to modify.
+linux-386 sha256 15c8c9c96ae1dfc1fff8ce5469eecb60dded782f6b8b7ba37d9edbeca443017e
+linux-amd64 sha256 df37ffc2588e345a31ca790d773b6136fedbd2efbf9a34cb735dd34b6891c16c
+linux-arm64 sha256 650f2a045f8587062a16299a650aa24ba5c5c0652585a2d9bd56594369d5f99e
+linux-armv6l sha256 61b657c860ddc39d3286ced073c843852b1dafc0222af0bdc22ad988b289d733
+linux-mips64 sha256 ab58e3886e8e0805c6c19e58b2f5a96e9fa3921c9dd1fc3ee956bf07b35993df
+linux-mips64le sha256 acac10fdb9fd459ef318e6b10b971471e2b6814e4a7fe14f119a15bd89b4b163
+linux-mipsle sha256 a2d31d02c838cbd849b068fa371ec278a836d2f184a90fb9d1a63271cf152d82
+linux-ppc64 sha256 a6f5a84de095bbe986dda8a16d17525280fc448d63347fd240c71d1cead78036
+linux-ppc64le sha256 77afc938916f84eec342acaabb8e32a845eac72e23b9db018cdf09fc31df585b
+linux-s390x sha256 86dd1092d1dc228d59d3862bb2d48ebe2e4f16a3c054fb04391f07d9d2b15d33
+mac-amd64 sha256 4d015791ed6f03f305cf6a5a673a447e5c47ff5fdb701f43f99fba9ca73e61f8
+windows-386 sha256 b8102c9a1b93915c128e7577c89fd77991ab83d52c356913e56ea505ab338735
+windows-amd64 sha256 a117e3984c111c68698faf91815c4b7d374404fa82dff318aadb9f2f0582ca8d
+`
+)
+
+var (
+ cipdPlatform string
+ cipdOS string
+ cipdArch string
+ cipdBinary string
+ selfUpdateOnce sync.Once
+)
+
+func init() {
+ cipdOS = runtime.GOOS
+ cipdArch = runtime.GOARCH
+ if cipdOS == "darwin" {
+ cipdOS = "mac"
+ }
+ if cipdArch == "arm" {
+ cipdArch = "armv6l"
+ }
+ cipdPlatform = cipdOS + "-" + cipdArch
+
jiriPath, err := osutil.Executable()
if err != nil {
- return "", err
+ // Could not locate jiri, leave cipdBinary empty
+ // It will be checked by bootstrap and reported
+ return
}
// Assume cipd binary is located in the same directory of jiri
jiriBinaryRoot := path.Dir(jiriPath)
- cipdBinary := path.Join(jiriBinaryRoot, "cipd")
- fileInfo, err := os.Stat(cipdBinary)
- if err != nil {
- if os.IsNotExist(err) {
- return "", fmt.Errorf("cipd binary was not found at %q", cipdBinary)
- }
- return "", err
- }
- // Check if cipd binary has execution permission
- if fileInfo.Mode()&0111 == 0 {
- return "", fmt.Errorf("cipd binary at %q is not executable", cipdBinary)
- }
- return cipdBinary, nil
+ cipdBinary = path.Join(jiriBinaryRoot, "cipd")
+}
+func fetchBinary(binaryPath, platform, version, digest string) error {
+ cipdURL := fmt.Sprintf("%s/client?platform=%s&version=%s", cipdBackend, platform, version)
+ data, err := fetchFile(cipdURL)
+ if err != nil {
+ return err
+ }
+ if verified, err := verifyDigest(data, digest); err != nil || !verified {
+ if err != nil {
+ return err
+ }
+ return errors.New("cipd failed integrity test")
+ }
+ // cipd binary verified. Save to disk
+ return writeFile(binaryPath, data)
+}
+
+// Bootstrap returns the path of a valid cipd binary. It will fetch cipd from
+// remote if a valid cipd binary is not found. It will update cipd if there
+// is a new version.
+func Bootstrap() (string, error) {
+ bootstrap := func() error {
+ // Fetch cipd digest
+ cipdDigest, _, err := fetchDigest(cipdPlatform)
+ if err != nil {
+ return err
+ }
+ if cipdBinary == "" {
+ return errors.New("cipd binary path was not set")
+ }
+ if err != nil {
+ return err
+ }
+ return fetchBinary(cipdBinary, cipdPlatform, cipdVersion, cipdDigest)
+ }
+
+ getCipd := func() (string, error) {
+ if cipdBinary == "" {
+ return "", errors.New("cipd binary path was not set")
+ }
+ fileInfo, err := os.Stat(cipdBinary)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return "", fmt.Errorf("cipd binary was not found at %q", cipdBinary)
+ }
+ return "", err
+ }
+ // Check if cipd binary has execution permission
+ if fileInfo.Mode()&0111 == 0 {
+ return "", fmt.Errorf("cipd binary at %q is not executable", cipdBinary)
+ }
+ return cipdBinary, nil
+ }
+
+ cipdPath, err := getCipd()
+ if err != nil {
+ // Could not find cipd binary or cipd is invalid
+ // Bootstrap it from scratch
+ if err := bootstrap(); err != nil {
+ return "", err
+ }
+ return cipdPath, nil
+ }
+ // cipd is found, do self update
+ var e error
+ selfUpdateOnce.Do(func() {
+ e = selfUpdate(cipdPath, cipdVersion)
+ })
+ if e != nil {
+ // Self update is unsuccessful, redo bootstrap
+ if err := bootstrap(); err != nil {
+ return "", err
+ }
+ }
+ return cipdPath, nil
+}
+
+func fetchDigest(platform string) (digest, method string, err error) {
+ var digestBuf bytes.Buffer
+ digestBuf.Write([]byte(cipdVersionDigest))
+ digestScanner := bufio.NewScanner(&digestBuf)
+ for digestScanner.Scan() {
+ curLine := digestScanner.Text()
+ if len(curLine) == 0 || curLine[0] == '#' {
+ // Skip comment or empty line
+ continue
+ }
+ fields := strings.Fields(curLine)
+ if len(fields) != 3 {
+ return "", "", errors.New("unsupported cipd digest file format")
+ }
+ if fields[0] == platform {
+ digest = fields[2]
+ method = fields[1]
+ err = nil
+ return
+ }
+ }
+ return "", "", errors.New("no matching platform found in cipd digest file")
+}
+
+func selfUpdate(cipdPath, cipdVersion string) error {
+ args := []string{"selfupdate", "-version", cipdVersion, "-service-url", cipdBackend}
+ command := exec.Command(cipdPath, args...)
+ return command.Run()
+}
+
+func writeFile(filePath string, data []byte) error {
+ tempFile, err := ioutil.TempFile(path.Dir(filePath), "cipd.*")
+ if err != nil {
+ return err
+ }
+ defer tempFile.Close()
+ defer os.Remove(tempFile.Name())
+ if _, err := tempFile.Write(data); err != nil {
+ // Write errors
+ return errors.New("I/O error while downloading cipd binary")
+ }
+ // Set mode to rwxr-xr-x
+ if err := tempFile.Chmod(0755); err != nil {
+ // Chmod errors
+ return errors.New("I/O error while adding executable permission to cipd binary")
+ }
+ tempFile.Close()
+ if err := os.Rename(tempFile.Name(), filePath); err != nil {
+ return err
+ }
+ return nil
+}
+
+func verifyDigest(data []byte, cipdDigest string) (bool, error) {
+ hash := sha256.Sum256(data)
+ hashString := fmt.Sprintf("%x", hash)
+ if hashString == strings.ToLower(cipdDigest) {
+ return true, nil
+ }
+ return false, nil
+}
+
+func getUserAgent() string {
+ ua := "jiri/" + version.GitCommit
+ if version.GitCommit == "" {
+ ua += "debug"
+ }
+ return ua
+}
+
+func fetchFile(url string) ([]byte, error) {
+ client := &http.Client{}
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("User-Agent", getUserAgent())
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ return ioutil.ReadAll(resp.Body)
}
// Ensure runs cipd binary's ensure funcationality over file. Fetched packages will be
-// saved to projectRoot directory. Parameter timeout is in minitues
+// saved to projectRoot directory. Parameter timeout is in minutes.
func Ensure(jirix *jiri.X, file, projectRoot string, timeout uint) error {
- cipdBinary, err := getCipdPath()
+ cipdPath, err := Bootstrap()
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Minute)
defer cancel()
args := []string{"ensure", "-ensure-file", file, "-root", projectRoot, "-log-level", "warning"}
- jirix.Logger.Debugf("Invoke cipd with %v", args)
- command := exec.CommandContext(ctx, cipdBinary, args...)
+ // Walkaround so tests do not have to create a new fake jirix, which would
+ // result in an import cycle
+ if jirix != nil {
+ jirix.Logger.Debugf("Invoke cipd with %v", args)
+ }
+ // Construct arguments and invoke cipd for ensure file
+ command := exec.CommandContext(ctx, cipdPath, args...)
+ // Add User-Agent info for cipd
+ command.Env = append(os.Environ(), "CIPD_HTTP_USER_AGENT_PREFIX="+getUserAgent())
command.Stdin = os.Stdin
command.Stdout = os.Stdout
command.Stderr = os.Stderr
diff --git a/cipd/cipd_test.go b/cipd/cipd_test.go
new file mode 100644
index 0000000..fdff7ed
--- /dev/null
+++ b/cipd/cipd_test.go
@@ -0,0 +1,166 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package cipd
+
+import (
+ "encoding/hex"
+ "io/ioutil"
+ "os"
+ "path"
+ "strings"
+ "testing"
+)
+
+const (
+ // Some random valid cipd version tags from infra/tools/cipd
+ cipdVersionForTestA = "git_revision:00e2d8b49a4e7505d1c71f19d15c9e7c5b9245a5"
+ cipdVersionForTestB = "git_revision:8fac632847b1ce0de3b57d16d0f2193625f4a4f0"
+)
+
+var (
+ // Digests generated by cipd selfupdate-roll ...
+ digestMapA = map[string]string{
+ "linux-amd64": "df37ffc2588e345a31ca790d773b6136fedbd2efbf9a34cb735dd34b6891c16c",
+ "linux-arm64": "650f2a045f8587062a16299a650aa24ba5c5c0652585a2d9bd56594369d5f99e",
+ "linux-armv6l": "61b657c860ddc39d3286ced073c843852b1dafc0222af0bdc22ad988b289d733",
+ "mac-amd64": "4d015791ed6f03f305cf6a5a673a447e5c47ff5fdb701f43f99fba9ca73e61f8",
+ }
+ digestMapB = map[string]string{
+ "linux-amd64": "bdc971fd2895c3771e0709d2a3ec5fcace69c59a3a9f9dc33ab76fbc2f777d40",
+ "linux-arm64": "e1d6aadc9bfc155e9088aa3de39b9d3311c7359f398f372b5ad1c308e25edfeb",
+ "linux-armv6l": "3ad97b47ecc1b358c8ebd1b0307087d354433d88f24bf8ece096fb05452837f9",
+ "mac-amd64": "167edadf7c7c019a40b9f7869a4c05b2d9834427dad68e295442ef9ebce88dba",
+ }
+)
+
+// TestFetchBinary tests fetchiBinary method by fetching a set of
+// cipd binaries. This test requires network access
+func TestFetchBinary(t *testing.T) {
+ tmpDir, err := ioutil.TempDir("", "jiri-test")
+ if err != nil {
+ t.Error("failed to create temp dir for testing")
+ }
+ defer os.RemoveAll(tmpDir)
+
+ tests := []struct {
+ version string
+ digest map[string]string
+ }{
+ {cipdVersionForTestA, digestMapA},
+ {cipdVersionForTestB, digestMapB},
+ }
+
+ for i, test := range tests {
+ for platform, digest := range test.digest {
+ cipdPath := path.Join(tmpDir, "cipd"+platform+test.version)
+ if err := fetchBinary(cipdPath, platform, test.version, digest); err != nil {
+ t.Errorf("test %d failed while retrieving cipd binary for platform %q on version %q with digest %q: %v", i, platform, test.version, digest, err)
+ }
+ }
+ }
+}
+
+func TestCipdVersion(t *testing.T) {
+ // Assume cipd version is always a git commit hash for now
+ versionStr := string(cipdVersion)
+ if len(versionStr) != len("git_revision:00e2d8b49a4e7505d1c71f19d15c9e7c5b9245a5") ||
+ !strings.HasPrefix(versionStr, "git_revision:") {
+ t.Errorf("unsupported cipd version tag: %q", versionStr)
+ }
+ versionHash := versionStr[len("git_revision:"):]
+ if _, err := hex.DecodeString(versionHash); err != nil {
+ t.Errorf("unsupported cipd version tag: %q", versionStr)
+ }
+}
+
+func TestFetchDigest(t *testing.T) {
+ tests := []string{
+ "linux-amd64",
+ "linux-arm64",
+ "linux-armv6l",
+ "mac-amd64",
+ }
+
+ for _, platform := range tests {
+ digest, _, err := fetchDigest(platform)
+ if err != nil {
+ t.Errorf("failed to retrieve cipd digest for platform %q due to error: %v", platform, err)
+ }
+ if _, err := hex.DecodeString(digest); err != nil {
+ t.Errorf("digest %q is not a valid hex string for platform %q", digest, platform)
+ }
+ }
+}
+
+func TestSelfUpdate(t *testing.T) {
+ tmpDir, err := ioutil.TempDir("", "jiri-test")
+ if err != nil {
+ t.Error("failed to create temp dir for testing")
+ }
+ defer os.RemoveAll(tmpDir)
+ // Bootstrap cipd to version A
+ cipdPath := path.Join(tmpDir, "cipd")
+ if err := fetchBinary(cipdPath, cipdPlatform, cipdVersionForTestA, digestMapA[cipdPlatform]); err != nil {
+ t.Errorf("failed to bootstrap cipd with version %q: %v", cipdVersionForTestA, err)
+ }
+ // Perform cipd self update to version B
+ if err := selfUpdate(cipdPath, cipdVersionForTestB); err != nil {
+ t.Errorf("failed to perform cipd self update: %v", err)
+ }
+ // Verify self updated cipd
+ cipdData, err := ioutil.ReadFile(cipdPath)
+ if err != nil {
+ t.Errorf("failed to read self-updated cipd binary: %v", err)
+ }
+ verified, err := verifyDigest(cipdData, digestMapB[cipdPlatform])
+ if err != nil {
+ t.Errorf("digest failed verification for platform %q on version %q", cipdPlatform, cipdVersionForTestB)
+ }
+ if !verified {
+ t.Errorf("self-updated cipd failed integrity test")
+ }
+}
+
+func TestEnsure(t *testing.T) {
+ cipdPath, err := Bootstrap()
+ if err != nil {
+ t.Errorf("bootstrap failed due to error: %v", err)
+ }
+ defer os.Remove(cipdPath)
+ // Write test ensure file
+ testEnsureFile, err := ioutil.TempFile("", "test_jiri*.ensure")
+ if err != nil {
+ t.Errorf("failed to create test ensure file: %v", err)
+ }
+ defer testEnsureFile.Close()
+ defer os.Remove(testEnsureFile.Name())
+ _, err = testEnsureFile.Write([]byte(`
+$ParanoidMode CheckPresence
+
+# GN
+gn/gn/${platform} git_revision:bdb0fd02324b120cacde634a9235405061c8ea06
+`))
+ if err != nil {
+ t.Errorf("failed to write test ensure file: %v", err)
+ }
+ testEnsureFile.Sync()
+ tmpDir, err := ioutil.TempDir("", "jiri-test")
+ if err != nil {
+ t.Error("failed to creat temp dir for testing")
+ }
+ defer os.RemoveAll(tmpDir)
+ // Invoke Ensure on test ensure file
+ if err := Ensure(nil, testEnsureFile.Name(), tmpDir, 30); err != nil {
+ t.Errorf("ensure failed due to error: %v", err)
+ }
+ // Check the existence downloaded package
+ gnPath := path.Join(tmpDir, "gn")
+ if _, err := os.Stat(gnPath); err != nil {
+ if os.IsNotExist(err) {
+ t.Errorf("fetched cipd package is not found at %q", gnPath)
+ }
+ t.Errorf("failed to execute os.Stat() on fetched cipd package due to error: %v", err)
+ }
+}