| // Copyright 2019 The Chromium 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 is an internal CIPD tool wrapper. |
| package cipd |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "net/http" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| |
| "go.chromium.org/luci/cipd/client/cipd" |
| "go.chromium.org/luci/common/errors" |
| ) |
| |
| const service = "https://chrome-infra-packages.appspot.com" |
| |
| // Package contains information about an installed package. |
| type Package struct { |
| Package string `json:"package"` |
| Pin Pin `json:"pin"` |
| Tracking string `json:"tracking"` |
| } |
| |
| // Pin contains information about an installed package instance. |
| type Pin struct { |
| Package string `json:"package"` |
| InstanceID string `json:"instance_id"` |
| } |
| |
| type installedPackagesFunc = func(root string) ([]Package, error) |
| type installedPackagesInnerFunc = func(root string) ([]byte, error) |
| type jsonCmdOutputFunc = func(cmd *exec.Cmd) ([]byte, error) |
| |
| // InstalledPackages returns information about installed CIPD packages. |
| func InstalledPackages(applicationName string) installedPackagesFunc { |
| return func(root string) ([]Package, error) { |
| out, err := installedPackages(applicationName)(root) |
| if err != nil { |
| return nil, errors.Annotate(err, "get CIPD packages for %s", root).Err() |
| } |
| pkgs, err := unmarshalPackages(out) |
| if err != nil { |
| return nil, errors.Annotate(err, "get CIPD packages for %s", root).Err() |
| } |
| return pkgs, nil |
| } |
| } |
| |
| // installedPackages returns the raw JSON from running a cipd installed command. |
| func installedPackages(applicationName string) installedPackagesInnerFunc { |
| return func(root string) ([]byte, error) { |
| cmd := exec.Command("cipd", "installed", "-root", root) |
| return jsonCmdOutput(applicationName)(cmd) |
| } |
| } |
| |
| func unmarshalPackages(jsonData []byte) ([]Package, error) { |
| var obj struct { |
| Result map[string][]Package `json:"result"` |
| } |
| if err := json.Unmarshal(jsonData, &obj); err != nil { |
| return nil, errors.Annotate(err, "unmarshal packages").Err() |
| } |
| if obj.Result == nil { |
| return nil, errors.Reason("unmarshal packages: bad JSON").Err() |
| } |
| pkgs, ok := obj.Result[""] |
| if !ok { |
| return nil, errors.Reason("unmarshal packages: bad JSON").Err() |
| } |
| return pkgs, nil |
| } |
| |
| // Run a cipd command that supports -json-output and return the raw JSON. |
| func jsonCmdOutput(applicationName string) jsonCmdOutputFunc { |
| return func(cmd *exec.Cmd) ([]byte, error) { |
| f, err := ioutil.TempFile("", fmt.Sprintf("%s-cipd-output", applicationName)) |
| if err != nil { |
| return nil, errors.Annotate(err, "JSON command output").Err() |
| } |
| defer os.Remove(f.Name()) |
| cmd.Args = append(cmd.Args, "-json-output", f.Name()) |
| if _, err = cmd.Output(); err != nil { |
| return nil, errors.Annotate(err, "JSON command output").Err() |
| } |
| out, err := ioutil.ReadAll(f) |
| if err != nil { |
| return nil, errors.Annotate(err, "JSON command output").Err() |
| } |
| return out, nil |
| } |
| } |
| |
| // FindPackage find the package by a given package name. |
| func FindPackage(packageName, cipdInstalledPath string) (*Package, error) { |
| errAnnotation := fmt.Sprintf("find package %s", packageName) |
| d, err := executableDir() |
| if err != nil { |
| return nil, errors.Annotate(err, errAnnotation).Err() |
| } |
| root, err := findCIPDRootDir(d) |
| if err != nil { |
| return nil, errors.Annotate(err, errAnnotation).Err() |
| } |
| pkgs, err := InstalledPackages(packageName)(root) |
| if err != nil { |
| return nil, errors.Annotate(err, errAnnotation).Err() |
| } |
| for _, p := range pkgs { |
| if !strings.HasPrefix(p.Package, cipdInstalledPath) { |
| continue |
| } |
| return &p, nil |
| } |
| return nil, errors.Reason(fmt.Sprintf("%s package: not found in %s", packageName, root)).Err() |
| } |
| |
| // DescribePackage returns information about a package instances. |
| func DescribePackage(ctx context.Context, pkg, version string) (*cipd.InstanceDescription, error) { |
| opts := cipd.ClientOptions{ |
| ServiceURL: service, |
| AnonymousClient: http.DefaultClient, |
| } |
| client, err := cipd.NewClient(opts) |
| if err != nil { |
| return nil, errors.Annotate(err, "describe package").Err() |
| } |
| pin, err := client.ResolveVersion(ctx, pkg, version) |
| if err != nil { |
| return nil, errors.Annotate(err, "describe package").Err() |
| } |
| d, err := client.DescribeInstance(ctx, pin, nil) |
| if err != nil { |
| return nil, errors.Annotate(err, "describe package").Err() |
| } |
| return d, nil |
| } |
| |
| // UpdatePackageToProd updates a given package by its cipd path to prod tag. |
| func UpdatePackageToProd(cipdInstalledPath string, outWriter io.Writer, errWriter io.Writer) error { |
| return UpdatePackage(cipdInstalledPath, "prod", outWriter, errWriter) |
| } |
| |
| // UpdatePackage updates a given package by its cipd path to given tag. |
| func UpdatePackage(cipdInstalledPath, refTag string, outWriter io.Writer, errWriter io.Writer) error { |
| d, err := executableDir() |
| if err != nil { |
| return err |
| } |
| root, err := findCIPDRootDir(d) |
| if err != nil { |
| return err |
| } |
| cmd := exec.Command("cipd", "ensure", "-root", root, "-ensure-file", "-") |
| cmd.Stdin = strings.NewReader(fmt.Sprintf("%s${platform} %s", cipdInstalledPath, refTag)) |
| cmd.Stdout = outWriter |
| cmd.Stderr = errWriter |
| if err := cmd.Run(); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| func findCIPDRootDir(dir string) (string, error) { |
| a, err := filepath.Abs(dir) |
| if err != nil { |
| return "", errors.Annotate(err, "find CIPD root dir").Err() |
| } |
| for d := a; d != "/"; d = filepath.Dir(d) { |
| if isCIPDRootDir(d) { |
| return d, nil |
| } |
| } |
| return "", errors.Reason("find CIPD root dir: no CIPD root above %s", dir).Err() |
| } |
| |
| func isCIPDRootDir(dir string) bool { |
| fi, err := os.Stat(filepath.Join(dir, ".cipd")) |
| if err != nil { |
| return false |
| } |
| return fi.Mode().IsDir() |
| } |
| |
| // executableDir returns the directory the current executable came |
| // from. |
| func executableDir() (string, error) { |
| p, err := os.Executable() |
| if err != nil { |
| return "", errors.Annotate(err, "get executable directory").Err() |
| } |
| return filepath.Dir(p), nil |
| } |