Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(49)

Unified Diff: go/src/infra/tools/cipd/apps/cipd/friendly.go

Issue 1358533003: cipd: Implement 'init', 'install' and 'installed' subcommands. (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Created 5 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | go/src/infra/tools/cipd/apps/cipd/main.go » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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))
+}
« no previous file with comments | « no previous file | go/src/infra/tools/cipd/apps/cipd/main.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698