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

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') | go/src/infra/tools/cipd/apps/cipd/main.go » ('J')
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..f710d62ab26258f0755548bafe540dc298bc4285
--- /dev/null
+++ b/go/src/infra/tools/cipd/apps/cipd/friendly.go
@@ -0,0 +1,452 @@
+// 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"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "github.com/luci/luci-go/client/authcli"
+ "github.com/luci/luci-go/common/auth"
+
+ "github.com/maruel/subcommands"
+
+ "infra/tools/cipd"
+ "infra/tools/cipd/local"
+)
+
+// 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 {
tandrii(chromium) 2015/09/21 13:47:41 Good! I coded this for something already...
+ for {
+ if isSiteRoot(p) {
+ return p
+ }
+ parent := filepath.Dir(p)
+ if parent == p {
nodir 2015/09/21 22:26:58 this seems to be undocumented behavior. Works on l
Vadim Sh. 2015/09/29 01:43:33 Why check if you have the source code? :) https://
+ 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 of <p>/.cipd exists.
+func isSiteRoot(p string) bool {
+ fi, err := os.Stat(filepath.Join(p, local.SiteServiceDir))
+ return err == nil && fi.IsDir()
+}
+
+// siteConfig is stored in .cipd/config.json.
+type siteConfig 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 *siteConfig) read(path string) error {
+ *c = siteConfig{}
+ 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 *siteConfig) 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) (siteConfig, error) {
+ path := filepath.Join(siteRoot, local.SiteServiceDir, "config.json")
+ c := siteConfig{}
+ if err := c.read(path); err != nil && !os.IsNotExist(err) {
+ return c, err
+ }
+ return c, nil
+}
+
+// modifyConfig reads config file, calls callback to mutate it, then writes
+// it back.
+func modifyConfig(siteRoot string, cb func(cfg *siteConfig) error) error {
+ path := filepath.Join(siteRoot, local.SiteServiceDir, "config.json")
+ c := siteConfig{}
+ if err := c.read(path); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ if err := cb(&c); err != nil {
+ return err
+ }
+ return c.write(path)
+}
+
+type siteRootCB func(siteRoot string, cfg *siteConfig) error
+
+// doInSiteRoot finds site root directory, reads config and calls callback.
+// Is siteRoot is "", will find a site root based on current directory,
tandrii(chromium) 2015/09/21 13:47:41 s/Is/If
Vadim Sh. 2015/09/29 01:43:33 Done.
+// otherwise will use siteRoot.
+func doInSiteRoot(siteRoot string, cb siteRootCB) error {
nodir 2015/09/21 22:26:58 I think just returning siteRoot and cfg would be s
Vadim Sh. 2015/09/29 01:43:33 Done. Not sure why I went callback-warrior road. P
+ siteRoot, err := optionalSiteRoot(siteRoot)
+ if err != nil {
+ return err
+ }
+ cfg, err := readConfig(siteRoot)
+ if err != nil {
+ return err
+ }
+ return cb(siteRoot, &cfg)
+}
+
+type siteRootWithClientCB func(siteRoot string, cfg *siteConfig, c cipd.Client) error
+
+// doInSiteRootWithClient is like doInSiteRoot, but also initializes
+// cipd.Client object to talk to the backend.
+func doInSiteRootWithClient(siteRoot string, authFlags authcli.Flags, cb siteRootWithClientCB) error {
nodir 2015/09/21 22:26:59 I think translating this to (siteConfig) func cre
Vadim Sh. 2015/09/29 01:43:33 client needs authFlags. Moved into separate initCl
+ return doInSiteRoot(siteRoot, func(siteRoot string, cfg *siteConfig) error {
+ serviceOpts := ServiceOptions{
+ authFlags: authFlags,
+ serviceURL: cfg.ServiceURL,
+ }
+ client, err := serviceOpts.makeCipdClient(siteRoot)
+ if err != nil {
+ return err
+ }
+ return cb(siteRoot, cfg, client)
+ })
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 'init' subcommand.
+
+var cmdInit = &subcommands.Command{
+ UsageLine: "init [options]",
+ ShortDesc: "sets up a new site root directory to install packages to",
+ LongDesc: "Sets up a new site root directory to install packages to.",
+ CommandRun: func() subcommands.CommandRun {
+ c := &initRun{}
+ c.registerBaseFlags()
+ c.Flags.StringVar(&c.rootDir, "root", "", "Path to an installation site root directory.")
nodir 2015/09/21 22:26:59 optional: use "." as default so it is obvious for
Vadim Sh. 2015/09/29 01:43:33 Done.
+ c.Flags.BoolVar(&c.force, "force", false, "Create even if directory is not empty.")
+ c.Flags.StringVar(&c.serviceURL, "service-url", "", "URL of a backend to use instead of the default one.")
+ return c
+ },
+}
+
+type initRun struct {
+ Subcommand
+
+ rootDir string
+ force bool
+ serviceURL string
+}
+
+func (c *initRun) Run(a subcommands.Application, args []string) int {
+ if !c.init(args, 0, 0) {
+ return 1
+ }
+ rootDir, err := initSiteRoot(c.rootDir, c.force)
+ if err != nil {
+ return c.done(nil, err)
+ }
+ err = modifyConfig(rootDir, func(cfg *siteConfig) error {
+ cfg.ServiceURL = c.serviceURL
+ return nil
+ })
+ return c.done(rootDir, err)
+}
+
+func initSiteRoot(rootDir string, force bool) (string, error) {
+ // Default to cwd.
+ var err error
+ if rootDir == "" {
+ rootDir, err = os.Getwd()
+ } else {
+ rootDir, err = filepath.Abs(rootDir)
+ }
+ if err != nil {
+ return "", 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)
nodir 2015/09/21 22:26:59 Is json-output=- only for debugging? Otherwise Fpr
Vadim Sh. 2015/09/29 01:43:33 Error printing is done in done().
nodir 2015/09/30 21:03:05 Acknowledged.
+ if !force {
tandrii(chromium) 2015/09/21 13:47:41 This is extra meaning for force flag then: doc abo
Vadim Sh. 2015/09/29 01:43:33 Updated flag help string.
+ return "", errors.New(msg)
+ }
+ fmt.Printf("Warning: %s.\n", msg)
+ }
+
+ // Attempting to use in a non empty directory?
+ items, err := ioutil.ReadDir(rootDir)
+ if err != nil && !os.IsNotExist(err) {
+ return "", err
+ }
+ if len(items) != 0 {
nodir 2015/09/21 22:26:59 what if err != nil?
Vadim Sh. 2015/09/29 01:43:33 err != nil here only if os.IsNotExists(err) is tru
nodir 2015/09/30 21:03:05 Ack. I read them incorrectly
+ msg := fmt.Sprintf("directory %s is not empty", rootDir)
+ if !force {
+ return "", errors.New(msg)
+ }
+ fmt.Printf("Warning: %s.\n", msg)
+ }
+
+ // Good to go.
+ if err = os.MkdirAll(filepath.Join(rootDir, local.SiteServiceDir), 0777); err != nil {
+ return "", err
+ }
+ fmt.Printf("Site root initialized at %s.\n", rootDir)
+ return rootDir, nil
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// '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.Flags.StringVar(&c.rootDir, "root", "", "Path to an installation site root directory.")
+ c.Flags.BoolVar(&c.force, "force", false, "Refetch and reinstall even if already installed.")
+ return c
+ },
+}
+
+type installRun struct {
+ Subcommand
+ authFlags authcli.Flags
+
+ rootDir string
+ 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.
+ rootDir, err := optionalSiteRoot(c.rootDir)
+ if err != nil {
+ rootDir, err = initSiteRoot(c.rootDir, false)
+ if err != nil {
+ err = fmt.Errorf("can't auto initialize cipd site root (%s), use 'init'", err)
+ return c.done(nil, err)
+ }
+ }
+
+ var output pinOrError
+ err = doInSiteRootWithClient(rootDir, c.authFlags,
+ func(siteRoot string, cfg *siteConfig, client cipd.Client) error {
+ output = installOrUpdate(pkgName, version, siteRoot, c.force, cfg, client)
+ return nil
+ })
+ return c.doneWithPins([]pinOrError{output}, err)
+}
+
+func installOrUpdate(pkgName, version, siteRoot string, force bool, cfg *siteConfig, client cipd.Client) pinOrError {
+ // Figure out what exactly to install.
+ if version == "" {
+ version = cfg.DefaultVersion
+ }
+ if version == "" {
+ version = "latest"
+ }
+ pin, err := client.ResolveVersion(pkgName, version)
nodir 2015/09/21 22:26:58 could use cfg.makeCipdClient to get client
Vadim Sh. 2015/09/29 01:43:33 did something else
+ if err != nil {
+ return pinOrError{
+ Pkg: pkgName,
+ Err: err.Error(),
+ }
+ }
+
+ // Already installed?
+ skipInstall := false
nodir 2015/09/21 22:26:59 I'd prefer positive name: forceInstall := true
Vadim Sh. 2015/09/29 01:43:33 Done.
+ if !force {
+ d := local.NewDeployer(siteRoot, log)
+ existing, err := d.CheckDeployed(pkgName)
+ if err == nil && existing == pin {
+ fmt.Printf("Package %s is up-to-date.\n", pkgName)
+ skipInstall = true
+ }
+ }
+
+ // Go for it.
+ if !skipInstall {
nodir 2015/09/21 22:26:58 negative on negative
Vadim Sh. 2015/09/29 01:43:33 Done.
+ fmt.Printf("Installing %s (version %q)...\n", pkgName, version)
+ if err = client.FetchAndDeployInstance(pin); err != nil {
+ return pinOrError{
+ Pkg: pkgName,
+ Err: err.Error(),
+ }
+ }
+ }
+
+ // Update config saying what version to track. Remove tracking if an exact
+ // instance ID was requested.
+ trackedVersion := ""
+ if version != pin.InstanceID {
+ trackedVersion = version
+ }
+ err = modifyConfig(siteRoot, func(cfg *siteConfig) 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, pin.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 pinOrError{
+ Pkg: pkgName,
+ Err: err.Error(),
+ }
+ }
+
+ // Success.
+ return pinOrError{
+ Pkg: pkgName,
+ Pin: &pin,
+ Tracking: trackedVersion,
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// '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.Flags.StringVar(&c.rootDir, "root", "", "Path to an installation site root directory.")
+ return c
+ },
+}
+
+type installedRun struct {
+ Subcommand
+
+ rootDir string
+}
+
+func (c *installedRun) Run(a subcommands.Application, args []string) int {
+ if !c.init(args, 0, -1) {
+ return 1
+ }
+ var output []pinOrError
+ err := doInSiteRoot(c.rootDir, func(siteRoot string, cfg *siteConfig) error {
+ var err error
+ output, err = findInstalled(args, siteRoot, cfg)
+ return err
+ })
+ return c.doneWithPins(output, err)
+}
+
+func findInstalled(pkgs []string, siteRoot string, cfg *siteConfig) ([]pinOrError, error) {
+ d := local.NewDeployer(siteRoot, log)
+
+ // List all?
+ if len(pkgs) == 0 {
+ pins, err := d.FindDeployed()
+ if err != nil {
+ return nil, err
+ }
+ output := make([]pinOrError, len(pins))
+ for i, pin := range pins {
+ cpy := pin
+ output[i] = pinOrError{
+ Pkg: pin.PackageName,
+ Pin: &cpy,
+ Tracking: cfg.TrackedVersions[pin.PackageName],
+ }
+ }
+ return output, nil
+ }
+
+ // List specific packages only.
+ output := make([]pinOrError, len(pkgs))
+ for i, pkgName := range pkgs {
+ pin, err := d.CheckDeployed(pkgName)
+ if err == nil {
+ output[i] = pinOrError{
+ Pkg: pkgName,
+ Pin: &pin,
+ Tracking: cfg.TrackedVersions[pkgName],
+ }
+ } else {
+ output[i] = pinOrError{
+ Pkg: pkgName,
+ Err: err.Error(),
+ Tracking: cfg.TrackedVersions[pkgName],
+ }
+ }
+ }
+ return output, nil
+}
« no previous file with comments | « no previous file | go/src/infra/tools/cipd/apps/cipd/main.go » ('j') | go/src/infra/tools/cipd/apps/cipd/main.go » ('J')

Powered by Google App Engine
This is Rietveld 408576698