| Index: go/src/infra/tools/cipd/apps/cipd/friendly.go
|
| diff --git a/go/src/infra/tools/cipd/apps/cipd/friendly.go b/go/src/infra/tools/cipd/apps/cipd/friendly.go
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..9a5daf994d0e04939cacc9d882695a02bbfa60d9
|
| --- /dev/null
|
| +++ b/go/src/infra/tools/cipd/apps/cipd/friendly.go
|
| @@ -0,0 +1,497 @@
|
| +// Copyright 2015 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 main
|
| +
|
| +import (
|
| + "encoding/json"
|
| + "errors"
|
| + "flag"
|
| + "fmt"
|
| + "io/ioutil"
|
| + "os"
|
| + "path/filepath"
|
| +
|
| + "github.com/luci/luci-go/client/authcli"
|
| + "github.com/luci/luci-go/common/auth"
|
| + "github.com/luci/luci-go/common/logging"
|
| +
|
| + "github.com/maruel/subcommands"
|
| +
|
| + "infra/tools/cipd"
|
| + "infra/tools/cipd/local"
|
| +)
|
| +
|
| +////////////////////////////////////////////////////////////////////////////////
|
| +// Site root path resolution.
|
| +
|
| +// findSiteRoot returns a directory R such as R/.cipd exists and p is inside
|
| +// R or p is R. Returns empty string if no such directory.
|
| +func findSiteRoot(p string) string {
|
| + for {
|
| + if isSiteRoot(p) {
|
| + return p
|
| + }
|
| + // Dir returns "/" (or C:\\) when it encounters the root directory. It is
|
| + // the only case when Dir(...) return value ends with separator.
|
| + parent := filepath.Dir(p)
|
| + if parent[len(parent)-1] == filepath.Separator {
|
| + // It is possible disk root has .cipd directory, check it.
|
| + if isSiteRoot(parent) {
|
| + return parent
|
| + }
|
| + return ""
|
| + }
|
| + p = parent
|
| + }
|
| +}
|
| +
|
| +// optionalSiteRoot takes a path to a site root or an empty string. If some
|
| +// path is given, it normalizes it and ensures that it is indeed a site root
|
| +// directory. If empty string is given, it discovers a site root for current
|
| +// directory.
|
| +func optionalSiteRoot(siteRoot string) (string, error) {
|
| + if siteRoot == "" {
|
| + cwd, err := os.Getwd()
|
| + if err != nil {
|
| + return "", err
|
| + }
|
| + siteRoot = findSiteRoot(cwd)
|
| + if siteRoot == "" {
|
| + return "", fmt.Errorf("directory %s is not in a site root, use 'init' to create one", cwd)
|
| + }
|
| + return siteRoot, nil
|
| + }
|
| + siteRoot, err := filepath.Abs(siteRoot)
|
| + if err != nil {
|
| + return "", err
|
| + }
|
| + if !isSiteRoot(siteRoot) {
|
| + return "", fmt.Errorf("directory %s doesn't look like a site root, use 'init' to create one", siteRoot)
|
| + }
|
| + return siteRoot, nil
|
| +}
|
| +
|
| +// isSiteRoot returns true if <p>/.cipd exists.
|
| +func isSiteRoot(p string) bool {
|
| + fi, err := os.Stat(filepath.Join(p, local.SiteServiceDir))
|
| + return err == nil && fi.IsDir()
|
| +}
|
| +
|
| +////////////////////////////////////////////////////////////////////////////////
|
| +// Config file parsing.
|
| +
|
| +// installationSiteConfig is stored in .cipd/config.json.
|
| +type installationSiteConfig struct {
|
| + // ServiceURL is https://<hostname> of a backend to use by default.
|
| + ServiceURL string
|
| + // DefaultVersion is what version to install if not specified.
|
| + DefaultVersion string
|
| + // TrackedVersions is mapping package name -> version to use in 'update'.
|
| + TrackedVersions map[string]string
|
| +}
|
| +
|
| +// read loads JSON from given path.
|
| +func (c *installationSiteConfig) read(path string) error {
|
| + *c = installationSiteConfig{}
|
| + r, err := os.Open(path)
|
| + if err != nil {
|
| + return err
|
| + }
|
| + defer r.Close()
|
| + return json.NewDecoder(r).Decode(c)
|
| +}
|
| +
|
| +// write dumps JSON to given path.
|
| +func (c *installationSiteConfig) write(path string) error {
|
| + blob, err := json.MarshalIndent(c, "", "\t")
|
| + if err != nil {
|
| + return err
|
| + }
|
| + return ioutil.WriteFile(path, blob, 0666)
|
| +}
|
| +
|
| +// readConfig reads config, returning default one if missing.
|
| +func readConfig(siteRoot string) (installationSiteConfig, error) {
|
| + path := filepath.Join(siteRoot, local.SiteServiceDir, "config.json")
|
| + c := installationSiteConfig{}
|
| + if err := c.read(path); err != nil && !os.IsNotExist(err) {
|
| + return c, err
|
| + }
|
| + return c, nil
|
| +}
|
| +
|
| +////////////////////////////////////////////////////////////////////////////////
|
| +// High level wrapper around site root.
|
| +
|
| +// installationSite represents a site root directory with config and optional
|
| +// cipd.Client instance configured to install packages into that root.
|
| +type installationSite struct {
|
| + siteRoot string // path to a site root directory
|
| + cfg *installationSiteConfig // parsed .cipd/config.json file
|
| + client cipd.Client // initialized by initClient()
|
| + log logging.Logger
|
| +}
|
| +
|
| +// getInstallationSite finds site root directory, reads config and constructs
|
| +// installationSite object. If siteRoot is "", will find a site root based on
|
| +// the current directory, otherwise will use siteRoot. Doesn't create any new
|
| +// files or directories, just reads what's on disk.
|
| +func getInstallationSite(siteRoot string) (*installationSite, error) {
|
| + siteRoot, err := optionalSiteRoot(siteRoot)
|
| + if err != nil {
|
| + return nil, err
|
| + }
|
| + cfg, err := readConfig(siteRoot)
|
| + if err != nil {
|
| + return nil, err
|
| + }
|
| + return &installationSite{siteRoot, &cfg, nil, log}, nil
|
| +}
|
| +
|
| +// initInstallationSite creates new site root directory on disk and returns
|
| +// corresponding *installationSite object. It does a bunch of sanity checks
|
| +// (like whether rootDir is empty) that are skipped if 'force' is set to true.
|
| +func initInstallationSite(rootDir string, force bool) (*installationSite, error) {
|
| + rootDir, err := filepath.Abs(rootDir)
|
| + if err != nil {
|
| + return nil, err
|
| + }
|
| +
|
| + // rootDir is inside an existing site root?
|
| + existing := findSiteRoot(rootDir)
|
| + if existing != "" {
|
| + msg := fmt.Sprintf("directory %s is already inside a site root (%s)", rootDir, existing)
|
| + if !force {
|
| + return nil, errors.New(msg)
|
| + }
|
| + fmt.Fprintf(os.Stderr, "Warning: %s.\n", msg)
|
| + }
|
| +
|
| + // Attempting to use in a non empty directory?
|
| + items, err := ioutil.ReadDir(rootDir)
|
| + if err != nil && !os.IsNotExist(err) {
|
| + return nil, err
|
| + }
|
| + if len(items) != 0 {
|
| + msg := fmt.Sprintf("directory %s is not empty", rootDir)
|
| + if !force {
|
| + return nil, errors.New(msg)
|
| + }
|
| + fmt.Fprintf(os.Stderr, "Warning: %s.\n", msg)
|
| + }
|
| +
|
| + // Good to go.
|
| + if err = os.MkdirAll(filepath.Join(rootDir, local.SiteServiceDir), 0777); err != nil {
|
| + return nil, err
|
| + }
|
| + site, err := getInstallationSite(rootDir)
|
| + if err != nil {
|
| + return nil, err
|
| + }
|
| + fmt.Printf("Site root initialized at %s.\n", rootDir)
|
| + return site, nil
|
| +}
|
| +
|
| +// initClient initializes cipd.Client to use to talk to backend. Can be called
|
| +// only once. Use it directly via site.client.
|
| +func (site *installationSite) initClient(authFlags authcli.Flags) (err error) {
|
| + if site.client != nil {
|
| + return errors.New("client is already initialized")
|
| + }
|
| + serviceOpts := ServiceOptions{
|
| + authFlags: authFlags,
|
| + serviceURL: site.cfg.ServiceURL,
|
| + }
|
| + site.client, err = serviceOpts.makeCipdClient(site.siteRoot)
|
| + return
|
| +}
|
| +
|
| +// modifyConfig reads config file, calls callback to mutate it, then writes
|
| +// it back.
|
| +func (site *installationSite) modifyConfig(cb func(cfg *installationSiteConfig) error) error {
|
| + path := filepath.Join(site.siteRoot, local.SiteServiceDir, "config.json")
|
| + c := installationSiteConfig{}
|
| + if err := c.read(path); err != nil && !os.IsNotExist(err) {
|
| + return err
|
| + }
|
| + if err := cb(&c); err != nil {
|
| + return err
|
| + }
|
| + return c.write(path)
|
| +}
|
| +
|
| +// installedPackages discovers versions of packages installed in the site. If
|
| +// pkgs is empty array, it returns list of all installed packages.
|
| +func (site *installationSite) installedPackages(pkgs []string) ([]pinInfo, error) {
|
| + d := local.NewDeployer(site.siteRoot, site.log)
|
| +
|
| + // List all?
|
| + if len(pkgs) == 0 {
|
| + pins, err := d.FindDeployed()
|
| + if err != nil {
|
| + return nil, err
|
| + }
|
| + output := make([]pinInfo, len(pins))
|
| + for i, pin := range pins {
|
| + cpy := pin
|
| + output[i] = pinInfo{
|
| + Pkg: pin.PackageName,
|
| + Pin: &cpy,
|
| + Tracking: site.cfg.TrackedVersions[pin.PackageName],
|
| + }
|
| + }
|
| + return output, nil
|
| + }
|
| +
|
| + // List specific packages only.
|
| + output := make([]pinInfo, len(pkgs))
|
| + for i, pkgName := range pkgs {
|
| + pin, err := d.CheckDeployed(pkgName)
|
| + if err == nil {
|
| + output[i] = pinInfo{
|
| + Pkg: pkgName,
|
| + Pin: &pin,
|
| + Tracking: site.cfg.TrackedVersions[pkgName],
|
| + }
|
| + } else {
|
| + output[i] = pinInfo{
|
| + Pkg: pkgName,
|
| + Tracking: site.cfg.TrackedVersions[pkgName],
|
| + Err: err.Error(),
|
| + }
|
| + }
|
| + }
|
| + return output, nil
|
| +}
|
| +
|
| +// installPackage installs (or updates) a package. If 'force' is true, it will
|
| +// reinstall the package even if it is already marked as installed at requested
|
| +// version. On errors returns (nil, error).
|
| +func (site *installationSite) installPackage(pkgName, version string, force bool) (*pinInfo, error) {
|
| + if site.client == nil {
|
| + return nil, errors.New("client is not initialized")
|
| + }
|
| +
|
| + // Figure out what exactly (what instance ID) to install.
|
| + if version == "" {
|
| + version = site.cfg.DefaultVersion
|
| + }
|
| + if version == "" {
|
| + version = "latest"
|
| + }
|
| + resolved, err := site.client.ResolveVersion(pkgName, version)
|
| + if err != nil {
|
| + return nil, err
|
| + }
|
| +
|
| + // Already installed?
|
| + doInstall := true
|
| + if !force {
|
| + d := local.NewDeployer(site.siteRoot, site.log)
|
| + existing, err := d.CheckDeployed(pkgName)
|
| + if err == nil && existing == resolved {
|
| + fmt.Printf("Package %s is up-to-date.\n", pkgName)
|
| + doInstall = false
|
| + }
|
| + }
|
| +
|
| + // Go for it.
|
| + if doInstall {
|
| + fmt.Printf("Installing %s (version %q)...\n", pkgName, version)
|
| + if err := site.client.FetchAndDeployInstance(resolved); err != nil {
|
| + return nil, err
|
| + }
|
| + }
|
| +
|
| + // Update config saying what version to track. Remove tracking if an exact
|
| + // instance ID was requested.
|
| + trackedVersion := ""
|
| + if version != resolved.InstanceID {
|
| + trackedVersion = version
|
| + }
|
| + err = site.modifyConfig(func(cfg *installationSiteConfig) error {
|
| + if cfg.TrackedVersions == nil {
|
| + cfg.TrackedVersions = map[string]string{}
|
| + }
|
| + if cfg.TrackedVersions[pkgName] != trackedVersion {
|
| + if trackedVersion == "" {
|
| + fmt.Printf("Package %s is now pinned to %q.\n", pkgName, resolved.InstanceID)
|
| + } else {
|
| + fmt.Printf("Package %s is now tracking %q.\n", pkgName, trackedVersion)
|
| + }
|
| + }
|
| + if trackedVersion == "" {
|
| + delete(cfg.TrackedVersions, pkgName)
|
| + } else {
|
| + cfg.TrackedVersions[pkgName] = trackedVersion
|
| + }
|
| + return nil
|
| + })
|
| + if err != nil {
|
| + return nil, err
|
| + }
|
| +
|
| + // Success.
|
| + return &pinInfo{
|
| + Pkg: pkgName,
|
| + Pin: &resolved,
|
| + Tracking: trackedVersion,
|
| + }, nil
|
| +}
|
| +
|
| +////////////////////////////////////////////////////////////////////////////////
|
| +// Common command line flags.
|
| +
|
| +// siteRootOptions defines command line flag for specifying existing site root
|
| +// directory. 'init' subcommand is NOT using it, since it creates a new site
|
| +// root, not reusing an existing one.
|
| +type siteRootOptions struct {
|
| + rootDir string
|
| +}
|
| +
|
| +func (opts *siteRootOptions) registerFlags(f *flag.FlagSet) {
|
| + f.StringVar(
|
| + &opts.rootDir, "root", "", "Path to an installation site root directory. "+
|
| + "If omitted will try to discovery it by examining parent directories.")
|
| +}
|
| +
|
| +////////////////////////////////////////////////////////////////////////////////
|
| +// 'init' subcommand.
|
| +
|
| +var cmdInit = &subcommands.Command{
|
| + UsageLine: "init [root dir] [options]",
|
| + ShortDesc: "sets up a new site root directory to install packages into",
|
| + LongDesc: "Sets up a new site root directory to install packages into.\n\n" +
|
| + "Uses current working directory by default.\n" +
|
| + "Unless -force is given, the new site root directory should be empty (or " +
|
| + "do not exist at all) and not be under some other existing site root. " +
|
| + "The command will create <root>/.cipd subdirectory with some " +
|
| + "configuration files. This directory is used by CIPD client to keep " +
|
| + "track of what is installed in the site root.",
|
| + CommandRun: func() subcommands.CommandRun {
|
| + c := &initRun{}
|
| + c.registerBaseFlags()
|
| + c.Flags.BoolVar(&c.force, "force", false, "Create the site root even if the directory is not empty or already under another site root directory.")
|
| + c.Flags.StringVar(&c.serviceURL, "service-url", "", "URL of a backend to use instead of the default one.")
|
| + return c
|
| + },
|
| +}
|
| +
|
| +type initRun struct {
|
| + Subcommand
|
| +
|
| + force bool
|
| + serviceURL string
|
| +}
|
| +
|
| +func (c *initRun) Run(a subcommands.Application, args []string) int {
|
| + if !c.init(args, 0, 1) {
|
| + return 1
|
| + }
|
| + rootDir := "."
|
| + if len(args) == 1 {
|
| + rootDir = args[0]
|
| + }
|
| + site, err := initInstallationSite(rootDir, c.force)
|
| + if err != nil {
|
| + return c.done(nil, err)
|
| + }
|
| + err = site.modifyConfig(func(cfg *installationSiteConfig) error {
|
| + cfg.ServiceURL = c.serviceURL
|
| + return nil
|
| + })
|
| + return c.done(site.siteRoot, err)
|
| +}
|
| +
|
| +////////////////////////////////////////////////////////////////////////////////
|
| +// 'install' subcommand.
|
| +
|
| +var cmdInstall = &subcommands.Command{
|
| + UsageLine: "install <package> [<version>] [options]",
|
| + ShortDesc: "installs or updates a package",
|
| + LongDesc: "Installs or updates a package.",
|
| + CommandRun: func() subcommands.CommandRun {
|
| + c := &installRun{}
|
| + c.registerBaseFlags()
|
| + c.authFlags.Register(&c.Flags, auth.Options{})
|
| + c.siteRootOptions.registerFlags(&c.Flags)
|
| + c.Flags.BoolVar(&c.force, "force", false, "Refetch and reinstall the package even if already installed.")
|
| + return c
|
| + },
|
| +}
|
| +
|
| +type installRun struct {
|
| + Subcommand
|
| + authFlags authcli.Flags
|
| + siteRootOptions
|
| +
|
| + force bool
|
| +}
|
| +
|
| +func (c *installRun) Run(a subcommands.Application, args []string) int {
|
| + if !c.init(args, 1, 2) {
|
| + return 1
|
| + }
|
| +
|
| + // Pkg and version to install.
|
| + pkgName := args[0]
|
| + version := ""
|
| + if len(args) == 2 {
|
| + version = args[1]
|
| + }
|
| +
|
| + // Auto initialize site root directory if necessary. Don't be too aggressive
|
| + // about it though (do not use force=true). Will do anything only if
|
| + // c.rootDir points to an empty directory.
|
| + var site *installationSite
|
| + rootDir, err := optionalSiteRoot(c.rootDir)
|
| + if err == nil {
|
| + site, err = getInstallationSite(rootDir)
|
| + } else {
|
| + site, err = initInstallationSite(c.rootDir, false)
|
| + if err != nil {
|
| + err = fmt.Errorf("can't auto initialize cipd site root (%s), use 'init'", err)
|
| + }
|
| + }
|
| + if err != nil {
|
| + return c.done(nil, err)
|
| + }
|
| + if err = site.initClient(c.authFlags); err != nil {
|
| + return c.done(nil, err)
|
| + }
|
| +
|
| + return c.done(site.installPackage(pkgName, version, c.force))
|
| +}
|
| +
|
| +////////////////////////////////////////////////////////////////////////////////
|
| +// 'installed' subcommand.
|
| +
|
| +var cmdInstalled = &subcommands.Command{
|
| + UsageLine: "installed [<package> <package> ...] [options]",
|
| + ShortDesc: "lists packages installed in the site root",
|
| + LongDesc: "Lists packages installed in the site root.",
|
| + CommandRun: func() subcommands.CommandRun {
|
| + c := &installedRun{}
|
| + c.registerBaseFlags()
|
| + c.siteRootOptions.registerFlags(&c.Flags)
|
| + return c
|
| + },
|
| +}
|
| +
|
| +type installedRun struct {
|
| + Subcommand
|
| + siteRootOptions
|
| +}
|
| +
|
| +func (c *installedRun) Run(a subcommands.Application, args []string) int {
|
| + if !c.init(args, 0, -1) {
|
| + return 1
|
| + }
|
| + site, err := getInstallationSite(c.rootDir)
|
| + if err != nil {
|
| + return c.done(nil, err)
|
| + }
|
| + return c.doneWithPins(site.installedPackages(args))
|
| +}
|
|
|