blob: e678b158c262b0030b590c84295bba94d539b5a3 [file] [log] [blame]
// Copyright 2014 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/maruel/subcommands"
"go.chromium.org/luci/auth"
"go.chromium.org/luci/client/versioncli"
"go.chromium.org/luci/common/cli"
"go.chromium.org/luci/common/data/stringset"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/flag/fixflagpos"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/logging/gologger"
"go.chromium.org/luci/common/retry/transient"
"go.chromium.org/luci/common/system/signals"
"go.chromium.org/luci/common/system/terminal"
"go.chromium.org/luci/auth/client/authcli"
api "go.chromium.org/luci/cipd/api/cipd/v1"
"go.chromium.org/luci/cipd/client/cipd"
"go.chromium.org/luci/cipd/client/cipd/builder"
"go.chromium.org/luci/cipd/client/cipd/deployer"
"go.chromium.org/luci/cipd/client/cipd/digests"
"go.chromium.org/luci/cipd/client/cipd/ensure"
"go.chromium.org/luci/cipd/client/cipd/fs"
"go.chromium.org/luci/cipd/client/cipd/pkg"
"go.chromium.org/luci/cipd/client/cipd/plugin/host"
"go.chromium.org/luci/cipd/client/cipd/reader"
"go.chromium.org/luci/cipd/client/cipd/template"
"go.chromium.org/luci/cipd/client/cipd/ui"
"go.chromium.org/luci/cipd/common"
)
// TODO(vadimsh): Add some tests.
// This is a killswitch that disables the fancy terminal progress bar UI in case
// it has some fatal bugs or a user has aversion towards it.
//
// Note that cipd.Client doesn't know anything about the UI implementation and
// thus this env var is defined here rather than in cipd/client.go like other
// env vars.
const envSimpleTerminalUI = "CIPD_SIMPLE_TERMINAL_UI"
func expandTemplate(tmpl string) (pkg string, err error) {
pkg, err = template.DefaultExpander().Expand(tmpl)
if err != nil {
err = commandLineError{err}
}
return
}
// Parameters carry default configuration values for a CIPD CLI client.
type Parameters struct {
// DefaultAuthOptions provide default values for authentication related
// options (most notably SecretsDir: a directory with token cache).
DefaultAuthOptions auth.Options
// ServiceURL is a backend URL to use by default.
ServiceURL string
}
////////////////////////////////////////////////////////////////////////////////
// Common subcommand functions.
// pinInfo contains information about single package pin inside some site root,
// or an error related to it. It is passed through channels when running batch
// operations and dumped to JSON results file in doneWithPins.
type pinInfo struct {
// Pkg is package name. Always set.
Pkg string `json:"package"`
// Pin is not nil if pin related operation succeeded. It contains instanceID.
Pin *common.Pin `json:"pin,omitempty"`
// Platform is set by 'ensure-file-verify' to a platform for this pin.
Platform string `json:"platform,omitempty"`
// Tracking is what ref is being tracked by that package in the site root.
Tracking string `json:"tracking,omitempty"`
// Err is not empty if pin related operation failed. Pin is nil in that case.
Err string `json:"error,omitempty"`
}
type instanceInfoWithRefs struct {
cipd.InstanceInfo
Refs []string `json:"refs,omitempty"`
}
// instancesOutput defines JSON format of 'cipd instances' output.
type instancesOutput struct {
Instances []instanceInfoWithRefs `json:"instances"`
}
// cipdSubcommand is a base of all CIPD subcommands. It defines some common
// flags, such as logging and JSON output parameters.
type cipdSubcommand struct {
subcommands.CommandRunBase
jsonOutput string
logConfig logging.Config
// TODO(dnj): Remove "verbose" flag once all current invocations of it are
// cleaned up and rolled out, as it is now deprecated in favor of "logConfig".
verbose bool
}
// ModifyContext implements cli.ContextModificator.
func (c *cipdSubcommand) ModifyContext(ctx context.Context) context.Context {
if c.verbose {
ctx = logging.SetLevel(ctx, logging.Debug)
} else {
ctx = c.logConfig.Set(ctx)
}
// Note: cli.Getenv is not effective here yet since the context is not fully
// initialized.
useSimpleUI := os.Getenv(envSimpleTerminalUI) == "1"
// If writing to a real terminal (rather than redirecting to a file) and not
// running at a non-default logging level, use a fancy UI with progress bars.
// It is more human readable, but doesn't preserve details of all operations
// in the terminal output.
if !useSimpleUI && logging.GetLevel(ctx) == logging.Info && terminal.IsTerminal(int(os.Stderr.Fd())) {
ctx = ui.SetImplementation(ctx, &ui.FancyImplementation{Out: os.Stderr})
}
return ctx
}
// registerBaseFlags registers common flags used by all subcommands.
func (c *cipdSubcommand) registerBaseFlags() {
// Minimum default logging level is Info. This accommodates subcommands that
// don't explicitly set the log level, resulting in the zero value (Debug).
if c.logConfig.Level < logging.Info {
c.logConfig.Level = logging.Info
}
c.Flags.StringVar(&c.jsonOutput, "json-output", "", "A `path` to write operation results to.")
c.Flags.BoolVar(&c.verbose, "verbose", false, "Enable more logging (deprecated, use -log-level=debug).")
c.logConfig.AddFlags(&c.Flags)
}
// checkArgs checks command line args.
//
// It ensures all required positional and flag-like parameters are set.
// Returns true if they are, or false (and prints to stderr) if not.
func (c *cipdSubcommand) checkArgs(args []string, minPosCount, maxPosCount int) bool {
// Check number of expected positional arguments.
if maxPosCount == 0 && len(args) != 0 {
c.printError(makeCLIError("unexpected arguments %v", args))
return false
}
if len(args) < minPosCount || (maxPosCount >= 0 && len(args) > maxPosCount) {
var err error
if minPosCount == maxPosCount {
err = makeCLIError("expecting %d positional argument, got %d instead", minPosCount, len(args))
} else {
if maxPosCount >= 0 {
err = makeCLIError(
"expecting from %d to %d positional arguments, got %d instead",
minPosCount, maxPosCount, len(args))
} else {
err = makeCLIError(
"expecting at least %d positional arguments, got %d instead",
minPosCount, len(args))
}
}
c.printError(err)
return false
}
// Check required unset flags.
unset := []*flag.Flag{}
c.Flags.VisitAll(func(f *flag.Flag) {
if strings.HasPrefix(f.DefValue, "<") && f.Value.String() == f.DefValue {
unset = append(unset, f)
}
})
if len(unset) != 0 {
missing := make([]string, len(unset))
for i, f := range unset {
missing[i] = f.Name
}
c.printError(makeCLIError("missing required flags: %v", missing))
return false
}
return true
}
// printError prints error to stderr (recognizing commandLineError).
func (c *cipdSubcommand) printError(err error) {
if _, ok := err.(commandLineError); ok {
fmt.Fprintf(os.Stderr, "Bad command line: %s.\n\n", err)
c.Flags.Usage()
return
}
if merr, _ := err.(errors.MultiError); len(merr) != 0 {
fmt.Fprintln(os.Stderr, "Errors:")
for _, err := range merr {
fmt.Fprintf(os.Stderr, " %s\n", err)
}
return
}
fmt.Fprintf(os.Stderr, "Error: %s.\n", err)
}
// writeJSONOutput writes result to JSON output file. It returns original error
// if it is non-nil.
func (c *cipdSubcommand) writeJSONOutput(result interface{}, err error) error {
// -json-output flag wasn't specified.
if c.jsonOutput == "" {
return err
}
// Prepare the body of the output file.
var body struct {
Error string `json:"error,omitempty"`
Result interface{} `json:"result,omitempty"`
}
if err != nil {
body.Error = err.Error()
}
body.Result = result
out, e := json.MarshalIndent(&body, "", " ")
if e != nil {
if err == nil {
err = e
} else {
fmt.Fprintf(os.Stderr, "Failed to serialize JSON output: %s\n", e)
}
return err
}
e = ioutil.WriteFile(c.jsonOutput, out, 0666)
if e != nil {
if err == nil {
err = e
} else {
fmt.Fprintf(os.Stderr, "Failed write JSON output to %s: %s\n", c.jsonOutput, e)
}
return err
}
return err
}
// done is called as a last step of processing a subcommand. It dumps command
// result (or error) to JSON output file, prints error message and generates
// process exit code.
func (c *cipdSubcommand) done(result interface{}, err error) int {
err = c.writeJSONOutput(result, err)
if err != nil {
c.printError(err)
return 1
}
return 0
}
// doneWithPins is a handy shortcut that prints a pinInfo slice and
// deduces process exit code based on presence of errors there.
//
// This just calls through to doneWithPinMap.
func (c *cipdSubcommand) doneWithPins(pins []pinInfo, err error) int {
return c.doneWithPinMap(map[string][]pinInfo{"": pins}, err)
}
// doneWithPinMap is a handy shortcut that prints the subdir->pinInfo map and
// deduces process exit code based on presence of errors there.
func (c *cipdSubcommand) doneWithPinMap(pins map[string][]pinInfo, err error) int {
if len(pins) == 0 {
if err == nil { // this error will be printed in c.done(...)
fmt.Println("No packages.")
}
} else {
printPinsAndError(pins)
}
if ret := c.done(pins, err); ret != 0 {
return ret
}
for _, infos := range pins {
if hasErrors(infos) {
return 1
}
}
return 0
}
// commandLineError is used to tag errors related to CLI.
type commandLineError struct {
error
}
// makeCLIError returns new commandLineError.
func makeCLIError(msg string, args ...interface{}) error {
return commandLineError{fmt.Errorf(msg, args...)}
}
////////////////////////////////////////////////////////////////////////////////
// maxThreadsOption mixin.
type maxThreadsOption struct {
maxThreads int
}
func (opts *maxThreadsOption) registerFlags(f *flag.FlagSet) {
f.IntVar(&opts.maxThreads, "max-threads", 0,
"Number of worker threads for extracting packages. If 0 or negative, uses CPU count.")
}
// loadMaxThreads should only be used by subcommands that do not instantiate
// the full CIPD client.
func (opts *maxThreadsOption) loadMaxThreads(ctx context.Context) (int, error) {
clientOpts := cipd.ClientOptions{MaxThreads: opts.maxThreads}
if err := clientOpts.LoadFromEnv(cli.MakeGetEnv(ctx)); err != nil {
return 0, err
}
return clientOpts.MaxThreads, nil
}
////////////////////////////////////////////////////////////////////////////////
// clientOptions mixin.
type rootDirFlag bool
const (
withRootDir rootDirFlag = true
withoutRootDir rootDirFlag = false
)
type maxThreadsFlag bool
const (
withMaxThreads maxThreadsFlag = true
withoutMaxThreads maxThreadsFlag = false
)
// clientOptions defines command line arguments related to CIPD client creation.
// Subcommands that need a CIPD client embed it.
type clientOptions struct {
hardcoded Parameters // whatever was passed to registerFlags(...)
serviceURL string // also mutated by loadEnsureFile
cacheDir string
maxThreads maxThreadsOption
rootDir string // used only if registerFlags got withRootDir arg
versions ensure.VersionsFile // mutated by loadEnsureFile
authFlags authcli.Flags
}
func (opts *clientOptions) resolvedServiceURL(ctx context.Context) string {
if opts.serviceURL != "" {
return opts.serviceURL
}
if v := cli.Getenv(ctx, cipd.EnvCIPDServiceURL); v != "" {
return v
}
return opts.hardcoded.ServiceURL
}
func (opts *clientOptions) registerFlags(f *flag.FlagSet, params Parameters, rootDir rootDirFlag, maxThreads maxThreadsFlag) {
opts.hardcoded = params
f.StringVar(&opts.serviceURL, "service-url", "",
fmt.Sprintf(`Backend URL. If provided via an "ensure" file, the URL in the file takes precedence. `+
`(default %s)`, params.ServiceURL))
f.StringVar(&opts.cacheDir, "cache-dir", "",
fmt.Sprintf("Directory for the shared cache (can also be set by %s env var).", cipd.EnvCacheDir))
if rootDir {
f.StringVar(&opts.rootDir, "root", "<path>", "Path to an installation site root directory.")
}
if maxThreads {
opts.maxThreads.registerFlags(f)
}
opts.authFlags.Register(f, params.DefaultAuthOptions)
}
func (opts *clientOptions) toCIPDClientOpts(ctx context.Context) (cipd.ClientOptions, error) {
authOpts, err := opts.authFlags.Options()
if err != nil {
return cipd.ClientOptions{}, err
}
client, err := auth.NewAuthenticator(ctx, auth.OptionalLogin, authOpts).Client()
if err != nil {
return cipd.ClientOptions{}, err
}
realOpts := cipd.ClientOptions{
Root: opts.rootDir,
CacheDir: opts.cacheDir,
Versions: opts.versions,
AuthenticatedClient: client,
MaxThreads: opts.maxThreads.maxThreads,
AnonymousClient: http.DefaultClient,
PluginHost: &host.Host{PluginsContext: ctx},
LoginInstructions: "run `cipd auth-login` to login or relogin",
}
if err := realOpts.LoadFromEnv(cli.MakeGetEnv(ctx)); err != nil {
return cipd.ClientOptions{}, err
}
realOpts.ServiceURL = opts.resolvedServiceURL(ctx)
return realOpts, nil
}
func (opts *clientOptions) makeCIPDClient(ctx context.Context) (cipd.Client, error) {
cipdOpts, err := opts.toCIPDClientOpts(ctx)
if err != nil {
return nil, err
}
return cipd.NewClient(cipdOpts)
}
////////////////////////////////////////////////////////////////////////////////
// inputOptions mixin.
// packageVars holds array of '-pkg-var' command line options.
type packageVars map[string]string
func (vars *packageVars) String() string {
return "key:value"
}
// Set is called by 'flag' package when parsing command line options.
func (vars *packageVars) Set(value string) error {
// <key>:<value> pair.
chunks := strings.SplitN(value, ":", 2)
if len(chunks) != 2 {
return makeCLIError("expecting <key>:<value> pair, got %q", value)
}
(*vars)[chunks[0]] = chunks[1]
return nil
}
// inputOptions defines command line arguments that specify where to get data
// for a new package and how to build it.
//
// Subcommands that build packages embed it.
type inputOptions struct {
// Path to *.yaml file with package definition.
packageDef string
vars packageVars
// Alternative to 'pkg-def'.
packageName string
inputDir string
installMode pkg.InstallMode
preserveModTime bool
preserveWritable bool
// Deflate compression level (if [1-9]) or 0 to disable compression.
//
// Default is 5.
compressionLevel int
}
func (opts *inputOptions) registerFlags(f *flag.FlagSet) {
// Set default vars (e.g. ${platform}). They may be overridden through flags.
defVars := template.DefaultExpander()
opts.vars = make(packageVars, len(defVars))
for k, v := range defVars {
opts.vars[k] = v
}
// Interface to accept package definition file.
f.StringVar(&opts.packageDef, "pkg-def", "", "A *.yaml file `path` that defines what to put into the package.")
f.Var(&opts.vars, "pkg-var", "A `key:value` with a variable accessible from package definition file (can be used multiple times).")
// Interface to accept a single directory (alternative to -pkg-def).
f.StringVar(&opts.packageName, "name", "", "Package `name` (unused with -pkg-def).")
f.StringVar(&opts.inputDir, "in", "", "A `path` to a directory with files to package (unused with -pkg-def).")
f.Var(&opts.installMode, "install-mode",
"How the package should be installed: \"copy\" or \"symlink\" (unused with -pkg-def).")
f.BoolVar(&opts.preserveModTime, "preserve-mtime", false,
"Preserve file's modification time (unused with -pkg-def).")
f.BoolVar(&opts.preserveWritable, "preserve-writable", false,
"Preserve file's writable permission bit (unused with -pkg-def).")
// Options for the builder.
f.IntVar(&opts.compressionLevel, "compression-level", 5,
"Deflate compression level [0-9]: 0 - disable, 1 - best speed, 9 - best compression.")
}
// prepareInput processes inputOptions by collecting all files to be added to
// a package and populating builder.Options. Caller is still responsible to fill
// out Output field of Options.
func (opts *inputOptions) prepareInput() (builder.Options, error) {
empty := builder.Options{}
if opts.compressionLevel < 0 || opts.compressionLevel > 9 {
return empty, makeCLIError("invalid -compression-level: must be in [0-9] set")
}
// Handle -name and -in if defined. Do not allow -pkg-def in that case, since
// it provides same information as -name and -in. Note that -pkg-var are
// ignored, even if defined. There's nothing to apply them to.
if opts.inputDir != "" {
if opts.packageName == "" {
return empty, makeCLIError("missing required flag: -name")
}
if opts.packageDef != "" {
return empty, makeCLIError("-pkg-def and -in can not be used together")
}
packageName, err := expandTemplate(opts.packageName)
if err != nil {
return empty, err
}
// Simply enumerate files in the directory.
files, err := fs.ScanFileSystem(opts.inputDir, opts.inputDir, nil, fs.ScanOptions{
PreserveModTime: opts.preserveModTime,
PreserveWritable: opts.preserveWritable,
})
if err != nil {
return empty, err
}
return builder.Options{
Input: files,
PackageName: packageName,
InstallMode: opts.installMode,
CompressionLevel: opts.compressionLevel,
}, nil
}
// Handle -pkg-def case. -in is "" (already checked), reject -name.
if opts.packageDef != "" {
if opts.packageName != "" {
return empty, makeCLIError("-pkg-def and -name can not be used together")
}
if opts.installMode != "" {
return empty, makeCLIError("-install-mode is ignored if -pkg-def is used")
}
if opts.preserveModTime {
return empty, makeCLIError("-preserve-mtime is ignored if -pkg-def is used")
}
if opts.preserveWritable {
return empty, makeCLIError("-preserve-writable is ignored if -pkg-def is used")
}
// Parse the file, perform variable substitution.
f, err := os.Open(opts.packageDef)
if err != nil {
return empty, err
}
defer f.Close()
pkgDef, err := builder.LoadPackageDef(f, opts.vars)
if err != nil {
return empty, err
}
// Scan the file system. Package definition may use path relative to the
// package definition file itself, so pass its location.
fmt.Println("Enumerating files to zip...")
files, err := pkgDef.FindFiles(filepath.Dir(opts.packageDef))
if err != nil {
return empty, err
}
return builder.Options{
Input: files,
PackageName: pkgDef.Package,
VersionFile: pkgDef.VersionFile(),
InstallMode: pkgDef.InstallMode,
CompressionLevel: opts.compressionLevel,
}, nil
}
// All command line options are missing.
return empty, makeCLIError("-pkg-def or -name/-in are required")
}
////////////////////////////////////////////////////////////////////////////////
// refsOptions mixin.
// refList holds an array of '-ref' command line options.
type refList []string
func (refs *refList) String() string {
return "ref"
}
// Set is called by 'flag' package when parsing command line options.
func (refs *refList) Set(value string) error {
err := common.ValidatePackageRef(value)
if err != nil {
return commandLineError{err}
}
*refs = append(*refs, value)
return nil
}
// refsOptions defines command line arguments for commands that accept a set
// of refs.
type refsOptions struct {
refs refList
}
func (opts *refsOptions) registerFlags(f *flag.FlagSet) {
f.Var(&opts.refs, "ref", "A `ref` to point to the package instance (can be used multiple times).")
}
////////////////////////////////////////////////////////////////////////////////
// tagsOptions mixin.
// tagList holds an array of '-tag' command line options.
type tagList []string
func (tags *tagList) String() string {
return "key:value"
}
// Set is called by 'flag' package when parsing command line options.
func (tags *tagList) Set(value string) error {
err := common.ValidateInstanceTag(value)
if err != nil {
return commandLineError{err}
}
*tags = append(*tags, value)
return nil
}
// tagsOptions defines command line arguments for commands that accept a set
// of tags.
type tagsOptions struct {
tags tagList
}
func (opts *tagsOptions) registerFlags(f *flag.FlagSet) {
f.Var(&opts.tags, "tag", "A `key:value` tag to attach to the package instance (can be used multiple times).")
}
////////////////////////////////////////////////////////////////////////////////
// metadataOptions mixin.
type metadataFlagValue struct {
key string
value string // either a literal value or a path to read it from
contentType string
}
type metadataList struct {
entries []metadataFlagValue
valueKind string // "value" or "path"
}
func (md *metadataList) String() string {
return "key:" + md.valueKind
}
// Set is called by 'flag' package when parsing command line options.
func (md *metadataList) Set(value string) error {
// Should have form key_with_possible_content_type:value.
chunks := strings.SplitN(value, ":", 2)
if len(chunks) != 2 {
return md.badFormatError()
}
// Extract content-type from within trailing '(...)', if present.
key, contentType, value := chunks[0], "", chunks[1]
switch l, r := strings.Index(key, "("), strings.LastIndex(key, ")"); {
case l == -1 && r == -1:
// no content type, this is fine
case l != -1 && r != -1 && l < r:
// The closing ')' should be the last character.
if !strings.HasSuffix(key, ")") {
return md.badFormatError()
}
key, contentType = key[:l], key[l+1:r]
default:
return md.badFormatError()
}
// Validate everything we can.
if err := common.ValidateInstanceMetadataKey(key); err != nil {
return commandLineError{err}
}
if err := common.ValidateContentType(contentType); err != nil {
return commandLineError{err}
}
if md.valueKind == "value" {
if err := common.ValidateInstanceMetadataLen(len(value)); err != nil {
return commandLineError{err}
}
}
md.entries = append(md.entries, metadataFlagValue{
key: key,
value: value,
contentType: contentType,
})
return nil
}
func (md *metadataList) badFormatError() error {
return makeCLIError("should have form key:%s or key(content-type):%s", md.valueKind, md.valueKind)
}
// metadataOptions defines command line arguments for commands that accept a set
// of metadata entries.
type metadataOptions struct {
metadata metadataList
metadataFromFile metadataList
}
func (opts *metadataOptions) registerFlags(f *flag.FlagSet) {
opts.metadata.valueKind = "value"
f.Var(&opts.metadata, "metadata",
"A metadata entry (`key:value` or key(content-type):value) to attach to the package instance (can be used multiple times).")
opts.metadataFromFile.valueKind = "path"
f.Var(&opts.metadataFromFile, "metadata-from-file",
"A metadata entry (`key:path` or key(content-type):path) to attach to the package instance (can be used multiple times). The path can be \"-\" to read from stdin.")
}
func (opts *metadataOptions) load(ctx context.Context) ([]cipd.Metadata, error) {
out := make([]cipd.Metadata, 0, len(opts.metadata.entries)+len(opts.metadataFromFile.entries))
// Convert -metadata to cipd.Metadata entries.
for _, md := range opts.metadata.entries {
entry := cipd.Metadata{
Key: md.key,
Value: []byte(md.value),
ContentType: md.contentType,
}
// The default content type for -metadata is text/plain (since values are
// supplied directly via the command line).
if entry.ContentType == "" {
entry.ContentType = "text/plain"
}
out = append(out, entry)
}
// Load -metadata-from-file entries. At most one `-metadata-from-file key:-`
// is allowed, we have only one stdin.
keyWithStdin := false
for _, md := range opts.metadataFromFile.entries {
if md.value == "-" {
if keyWithStdin {
return nil, makeCLIError("at most one -metadata-from-file can use \"-\" as a value")
}
keyWithStdin = true
}
entry := cipd.Metadata{
Key: md.key,
ContentType: md.contentType,
}
var err error
if entry.Value, err = loadMetadataFromFile(ctx, md.key, md.value); err != nil {
return nil, makeCLIError("when loading metadata from %q: %s", md.value, err)
}
// Guess the content type from the file extension and its body.
if entry.ContentType == "" {
entry.ContentType = guessMetadataContentType(md.value, entry.Value)
}
out = append(out, entry)
}
return out, nil
}
func loadMetadataFromFile(ctx context.Context, key, path string) ([]byte, error) {
var file *os.File
if path == "-" {
logging.Infof(ctx, "Reading metadata %q from the stdin...", key)
file = os.Stdin
} else {
logging.Infof(ctx, "Reading metadata %q from %q...", key, path)
var err error
file, err = os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
}
// Read at most MetadataMaxLen plus one more byte to detect true EOF.
buf := bytes.Buffer{}
switch _, err := io.CopyN(&buf, file, common.MetadataMaxLen+1); {
case err == nil:
// Successfully read more than needed => the file size is too large.
return nil, fmt.Errorf("the metadata value is too long, should be <=%d bytes", common.MetadataMaxLen)
case err != io.EOF:
// Failed with some unexpected read error.
return nil, err
default:
return buf.Bytes(), nil
}
}
func guessMetadataContentType(path string, val []byte) string {
switch strings.ToLower(filepath.Ext(path)) {
case ".json":
return "application/json"
case ".jwt":
return "application/jwt"
default:
return http.DetectContentType(val)
}
}
////////////////////////////////////////////////////////////////////////////////
// uploadOptions mixin.
// uploadOptions defines command line options for commands that upload packages.
type uploadOptions struct {
verificationTimeout time.Duration
}
func (opts *uploadOptions) registerFlags(f *flag.FlagSet) {
f.DurationVar(
&opts.verificationTimeout, "verification-timeout",
cipd.CASFinalizationTimeout, "Maximum time to wait for backend-side package hash verification.")
}
////////////////////////////////////////////////////////////////////////////////
// hashOptions mixin.
// allAlgos is used in the flag help text, it is "sha256, sha1, ...".
var allAlgos string
func init() {
algos := make([]string, 0, len(api.HashAlgo_name)-1)
for i := len(api.HashAlgo_name) - 1; i > 0; i-- {
algos = append(algos, strings.ToLower(api.HashAlgo_name[int32(i)]))
}
allAlgos = strings.Join(algos, ", ")
}
// hashAlgoFlag adapts api.HashAlgo to flag.Value interface.
type hashAlgoFlag api.HashAlgo
// String is called by 'flag' package when displaying default value of a flag.
func (ha *hashAlgoFlag) String() string {
return strings.ToLower(api.HashAlgo(*ha).String())
}
// Set is called by 'flag' package when parsing command line options.
func (ha *hashAlgoFlag) Set(value string) error {
val := api.HashAlgo_value[strings.ToUpper(value)]
if val == 0 {
return fmt.Errorf("unknown hash algo %q, should be one of: %s", value, allAlgos)
}
*ha = hashAlgoFlag(val)
return nil
}
// hashOptions defines -hash-algo flag that specifies hash algo to use for
// constructing instance IDs.
//
// Default value is given by common.DefaultHashAlgo.
//
// Not all algos may be accepted by the server.
type hashOptions struct {
algo hashAlgoFlag
}
func (opts *hashOptions) registerFlags(f *flag.FlagSet) {
opts.algo = hashAlgoFlag(common.DefaultHashAlgo)
f.Var(&opts.algo, "hash-algo", fmt.Sprintf("Algorithm to use for deriving package instance ID, one of: %s", allAlgos))
}
func (opts *hashOptions) hashAlgo() api.HashAlgo {
return api.HashAlgo(opts.algo)
}
////////////////////////////////////////////////////////////////////////////////
// ensureFileOptions mixin.
type legacyListFlag bool
const (
withLegacyListFlag legacyListFlag = true
withoutLegacyListFlag legacyListFlag = false
)
type ensureOutFlag bool
const (
withEnsureOutFlag ensureOutFlag = true
withoutEnsureOutFlag ensureOutFlag = false
)
type verifyingEnsureFile bool
const (
requireVerifyPlatforms verifyingEnsureFile = true
ignoreVerifyPlatforms verifyingEnsureFile = false
)
type versionFileOpt bool
const (
parseVersionsFile versionFileOpt = true
ignoreVersionsFile versionFileOpt = false
)
// ensureFileOptions defines -ensure-file flag that specifies a location of the
// "ensure file", which is a manifest that describes what should be installed
// into a site root.
type ensureFileOptions struct {
ensureFile string
ensureFileOut string // used only if registerFlags got withEnsureOutFlag arg
}
func (opts *ensureFileOptions) registerFlags(f *flag.FlagSet, out ensureOutFlag, list legacyListFlag) {
f.StringVar(&opts.ensureFile, "ensure-file", "<path>",
`An "ensure" file. See syntax described here: `+
`https://godoc.org/go.chromium.org/luci/cipd/client/cipd/ensure.`+
` Providing '-' will read from stdin.`)
if out {
f.StringVar(&opts.ensureFileOut, "ensure-file-output", "",
`A path to write an "ensure" file which is the fully-resolved version `+
`of the input ensure file for the current platform. This output will `+
`not contain any ${params} or $Settings other than $ServiceURL.`)
}
if list {
f.StringVar(&opts.ensureFile, "list", "<path>", "(DEPRECATED) A synonym for -ensure-file.")
}
}
// loadEnsureFile parses the ensure file and mutates clientOpts to point to a
// service URL specified in the ensure file.
func (opts *ensureFileOptions) loadEnsureFile(ctx context.Context, clientOpts *clientOptions, verifying verifyingEnsureFile, parseVers versionFileOpt) (*ensure.File, error) {
parsedFile, err := ensure.LoadEnsureFile(opts.ensureFile)
if err != nil {
return nil, err
}
// Prefer the ServiceURL from the file (if set), and log a warning if the user
// provided one on the command line that doesn't match the one in the file.
if parsedFile.ServiceURL != "" {
if clientOpts.serviceURL != "" && clientOpts.serviceURL != parsedFile.ServiceURL {
logging.Warningf(ctx, "serviceURL in ensure file != serviceURL on CLI (%q v %q). Using %q from file.",
parsedFile.ServiceURL, clientOpts.serviceURL, parsedFile.ServiceURL)
}
clientOpts.serviceURL = parsedFile.ServiceURL
}
if verifying && len(parsedFile.VerifyPlatforms) == 0 {
logging.Errorf(ctx,
"For this feature to work, verification platforms must be specified in "+
"the ensure file using one or more $VerifiedPlatform directives.")
return nil, errors.New("no verification platforms configured")
}
if parseVers && parsedFile.ResolvedVersions != "" {
clientOpts.versions, err = loadVersionsFile(parsedFile.ResolvedVersions, opts.ensureFile)
if err != nil {
return nil, err
}
logging.Debugf(ctx, "Using the resolved version file %q", filepath.Base(parsedFile.ResolvedVersions))
}
return parsedFile, nil
}
////////////////////////////////////////////////////////////////////////////////
// Support for running operations concurrently.
// batchOperation defines what to do with a packages matching a prefix.
type batchOperation struct {
client cipd.Client
packagePrefix string // a package name or a prefix
packages []string // packages to operate on, overrides packagePrefix
callback func(pkg string) (common.Pin, error)
}
// expandPkgDir takes a package name or '<prefix>/' and returns a list
// of matching packages (asking backend if necessary). Doesn't recurse, returns
// only direct children.
func expandPkgDir(ctx context.Context, c cipd.Client, packagePrefix string) ([]string, error) {
if !strings.HasSuffix(packagePrefix, "/") {
return []string{packagePrefix}, nil
}
pkgs, err := c.ListPackages(ctx, packagePrefix, false, false)
if err != nil {
return nil, err
}
// Skip directories.
var out []string
for _, p := range pkgs {
if !strings.HasSuffix(p, "/") {
out = append(out, p)
}
}
if len(out) == 0 {
return nil, fmt.Errorf("no packages under %s", packagePrefix)
}
return out, nil
}
// performBatchOperation expands a package prefix into a list of packages and
// calls callback for each of them (concurrently) gathering the results.
//
// Returns an error only if the prefix expansion fails. Errors from individual
// operations are returned through []pinInfo, use hasErrors to check them.
func performBatchOperation(ctx context.Context, op batchOperation) ([]pinInfo, error) {
op.client.BeginBatch(ctx)
defer op.client.EndBatch(ctx)
pkgs := op.packages
if len(pkgs) == 0 {
var err error
pkgs, err = expandPkgDir(ctx, op.client, op.packagePrefix)
if err != nil {
return nil, err
}
}
return callConcurrently(pkgs, func(pkg string) pinInfo {
pin, err := op.callback(pkg)
if err != nil {
return pinInfo{Pkg: pkg, Err: err.Error()}
}
return pinInfo{Pkg: pkg, Pin: &pin}
}), nil
}
func callConcurrently(pkgs []string, callback func(pkg string) pinInfo) []pinInfo {
// Push index through channel to make results ordered as 'pkgs'.
ch := make(chan struct {
int
pinInfo
})
for idx, pkg := range pkgs {
go func(idx int, pkg string) {
ch <- struct {
int
pinInfo
}{idx, callback(pkg)}
}(idx, pkg)
}
pins := make([]pinInfo, len(pkgs))
for i := 0; i < len(pkgs); i++ {
res := <-ch
pins[res.int] = res.pinInfo
}
return pins
}
func printPinsAndError(pinMap map[string][]pinInfo) {
for subdir, pins := range pinMap {
hasPins := false
hasErrors := false
for _, p := range pins {
if p.Err != "" {
hasErrors = true
} else if p.Pin != nil {
hasPins = true
}
}
subdirString := ""
if (hasPins || hasErrors) && (len(pinMap) > 1 || subdir != "") {
// only print this if it's not the root subdir, or there's more than one
// subdir in pinMap.
subdirString = fmt.Sprintf(" (subdir %q)", subdir)
}
if hasPins {
fmt.Printf("Packages%s:\n", subdirString)
for _, p := range pins {
if p.Err != "" || p.Pin == nil {
continue
}
plat := ""
if p.Platform != "" {
plat = fmt.Sprintf(" (for %s)", p.Platform)
}
tracking := ""
if p.Tracking != "" {
tracking = fmt.Sprintf(" (tracking %q)", p.Tracking)
}
fmt.Printf(" %s%s%s\n", p.Pin, plat, tracking)
}
}
if hasErrors {
fmt.Fprintf(os.Stderr, "Errors%s:\n", subdirString)
for _, p := range pins {
if p.Err != "" {
fmt.Fprintf(os.Stderr, " %s: %s.\n", p.Pkg, p.Err)
}
}
}
}
}
func hasErrors(pins []pinInfo) bool {
for _, p := range pins {
if p.Err != "" {
return true
}
}
return false
}
////////////////////////////////////////////////////////////////////////////////
// Ensure-file related helpers.
func resolveEnsureFile(ctx context.Context, f *ensure.File, clientOpts clientOptions) (map[string][]pinInfo, ensure.VersionsFile, error) {
client, err := clientOpts.makeCIPDClient(ctx)
if err != nil {
return nil, nil, err
}
defer client.Close(ctx)
out := ensure.VersionsFile{}
mu := sync.Mutex{}
resolver := cipd.Resolver{
Client: client,
VerifyPresence: true,
Visitor: func(pkg, ver, iid string) {
mu.Lock()
out.AddVersion(pkg, ver, iid)
mu.Unlock()
},
}
results, err := resolver.ResolveAllPlatforms(ctx, f)
if err != nil {
return nil, nil, err
}
return resolvedFilesToPinMap(results), out, nil
}
func resolvedFilesToPinMap(res map[template.Platform]*ensure.ResolvedFile) map[string][]pinInfo {
pinMap := map[string][]pinInfo{}
for plat, resolved := range res {
for subdir, resolvedPins := range resolved.PackagesBySubdir {
pins := pinMap[subdir]
for _, pin := range resolvedPins {
// Put a copy into 'pins', otherwise they all end up pointing to the
// same variable living in the outer scope.
pin := pin
pins = append(pins, pinInfo{
Pkg: pin.PackageName,
Pin: &pin,
Platform: plat.String(),
})
}
pinMap[subdir] = pins
}
}
// Sort pins by (package name, platform) for deterministic output.
for _, v := range pinMap {
sort.Slice(v, func(i, j int) bool {
if v[i].Pkg == v[j].Pkg {
return v[i].Platform < v[j].Platform
}
return v[i].Pkg < v[j].Pkg
})
}
return pinMap
}
func loadVersionsFile(path, ensureFile string) (ensure.VersionsFile, error) {
switch f, err := os.Open(path); {
case os.IsNotExist(err):
return nil, fmt.Errorf("the resolved versions file doesn't exist, "+
"use 'cipd ensure-file-resolve -ensure-file %q' to generate it", ensureFile)
case err != nil:
return nil, err
default:
defer f.Close()
return ensure.ParseVersionsFile(f)
}
}
func saveVersionsFile(path string, v ensure.VersionsFile) error {
buf := bytes.Buffer{}
if err := v.Serialize(&buf); err != nil {
return err
}
return ioutil.WriteFile(path, buf.Bytes(), 0666)
}
////////////////////////////////////////////////////////////////////////////////
// 'create' subcommand.
func cmdCreate(params Parameters) *subcommands.Command {
return &subcommands.Command{
UsageLine: "create [options]",
ShortDesc: "builds and uploads a package instance file",
LongDesc: "Builds and uploads a package instance file.",
CommandRun: func() subcommands.CommandRun {
c := &createRun{}
c.registerBaseFlags()
c.Opts.inputOptions.registerFlags(&c.Flags)
c.Opts.refsOptions.registerFlags(&c.Flags)
c.Opts.tagsOptions.registerFlags(&c.Flags)
c.Opts.metadataOptions.registerFlags(&c.Flags)
c.Opts.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.Opts.uploadOptions.registerFlags(&c.Flags)
c.Opts.hashOptions.registerFlags(&c.Flags)
return c
},
}
}
type createOpts struct {
inputOptions
refsOptions
tagsOptions
metadataOptions
clientOptions
uploadOptions
hashOptions
}
type createRun struct {
cipdSubcommand
Opts createOpts
}
func (c *createRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 0, 0) {
return 1
}
ctx := cli.GetContext(a, c, env)
return c.done(buildAndUploadInstance(ctx, &c.Opts))
}
func buildAndUploadInstance(ctx context.Context, opts *createOpts) (common.Pin, error) {
f, err := ioutil.TempFile("", "cipd_pkg")
if err != nil {
return common.Pin{}, err
}
defer func() {
f.Close()
os.Remove(f.Name())
}()
pin, err := buildInstanceFile(ctx, f.Name(), opts.inputOptions, opts.hashAlgo())
if err != nil {
return common.Pin{}, err
}
return registerInstanceFile(ctx, f.Name(), &pin, &registerOpts{
refsOptions: opts.refsOptions,
tagsOptions: opts.tagsOptions,
metadataOptions: opts.metadataOptions,
clientOptions: opts.clientOptions,
uploadOptions: opts.uploadOptions,
hashOptions: opts.hashOptions,
})
}
////////////////////////////////////////////////////////////////////////////////
// 'attach' subcommand.
func cmdAttach(params Parameters) *subcommands.Command {
return &subcommands.Command{
UsageLine: "attach <package or package prefix> -metadata key:value -metadata-from-file key:path -tag key:value -ref name [options]",
ShortDesc: "attaches tags, metadata and points refs to an instance",
LongDesc: `Attaches tags, metadata and points refs to an instance.
Note that this operation is not atomic. It attaches metadata first, then tags,
then moves refs one by one. Reattaching already attached data is not an error
though, so a failed operation can be safely retried.
`,
CommandRun: func() subcommands.CommandRun {
c := &attachRun{}
c.registerBaseFlags()
c.refsOptions.registerFlags(&c.Flags)
c.tagsOptions.registerFlags(&c.Flags)
c.metadataOptions.registerFlags(&c.Flags)
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.Flags.StringVar(&c.version, "version", "<version>",
"Package version to resolve. Could also be a tag or a ref.")
return c
},
}
}
type attachRun struct {
cipdSubcommand
refsOptions
tagsOptions
metadataOptions
clientOptions
version string
}
func (c *attachRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 1, 1) {
return 1
}
ctx := cli.GetContext(a, c, env)
md, err := c.metadataOptions.load(ctx)
if err != nil {
return c.done(nil, err)
}
if len(c.refs) == 0 && len(c.tags) == 0 && len(md) == 0 {
return c.done(nil, makeCLIError("no -tags, -refs or -metadata is provided"))
}
pkgPrefix, err := expandTemplate(args[0])
if err != nil {
return c.done(nil, err)
}
return c.doneWithPins(visitPins(ctx, &visitPinsArgs{
clientOptions: c.clientOptions,
packagePrefix: pkgPrefix,
version: c.version,
updatePin: func(client cipd.Client, pin common.Pin) error {
return attachAndMove(ctx, client, pin, md, c.tags, c.refs)
},
}))
}
////////////////////////////////////////////////////////////////////////////////
// 'ensure' subcommand.
func cmdEnsure(params Parameters) *subcommands.Command {
return &subcommands.Command{
UsageLine: "ensure [options]",
ShortDesc: "installs, removes and updates packages in one go",
LongDesc: `Installs, removes and updates packages in one go.
Prepare an 'ensure file' by listing packages and their versions, each on their
own line, e.g.:
some/package/name/${platform} version:1.2.3
other/package some_ref
Then use the ensure command to read this ensure file and 'ensure' that a given
folder has the packages at the versions specified:
cipd ensure -root a/directory -ensure-file ensure_file
For the full syntax of the ensure file, see:
https://go.chromium.org/luci/cipd/client/cipd/ensure
`,
CommandRun: func() subcommands.CommandRun {
c := &ensureRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withRootDir, withMaxThreads)
c.ensureFileOptions.registerFlags(&c.Flags, withEnsureOutFlag, withLegacyListFlag)
return c
},
}
}
type ensureRun struct {
cipdSubcommand
clientOptions
ensureFileOptions
}
func (c *ensureRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 0, 0) {
return 1
}
ctx := cli.GetContext(a, c, env)
ef, err := c.loadEnsureFile(ctx, &c.clientOptions, ignoreVerifyPlatforms, parseVersionsFile)
if err != nil {
return c.done(nil, err)
}
pins, _, err := ensurePackages(ctx, ef, c.ensureFileOut, false, c.clientOptions)
return c.done(pins, err)
}
func ensurePackages(ctx context.Context, ef *ensure.File, ensureFileOut string, dryRun bool, clientOpts clientOptions) (common.PinSliceBySubdir, cipd.ActionMap, error) {
client, err := clientOpts.makeCIPDClient(ctx)
if err != nil {
return nil, nil, err
}
defer client.Close(ctx)
client.BeginBatch(ctx)
defer client.EndBatch(ctx)
resolver := cipd.Resolver{Client: client}
resolved, err := resolver.Resolve(ctx, ef, template.DefaultExpander())
if err != nil {
return nil, nil, err
}
actions, err := client.EnsurePackages(ctx, resolved.PackagesBySubdir, &cipd.EnsureOptions{
Paranoia: resolved.ParanoidMode,
DryRun: dryRun,
})
if err != nil {
return nil, actions, err
}
if ensureFileOut != "" {
buf := bytes.Buffer{}
resolved.ServiceURL = clientOpts.resolvedServiceURL(ctx)
resolved.ParanoidMode = ""
if err = resolved.Serialize(&buf); err == nil {
err = ioutil.WriteFile(ensureFileOut, buf.Bytes(), 0666)
}
}
return resolved.PackagesBySubdir, actions, err
}
////////////////////////////////////////////////////////////////////////////////
// 'ensure-file-verify' subcommand.
func cmdEnsureFileVerify(params Parameters) *subcommands.Command {
return &subcommands.Command{
UsageLine: "ensure-file-verify [options]",
ShortDesc: "verifies packages in a manifest exist for all platforms",
LongDesc: "Verifies that the packages in the \"ensure\" file exist for all platforms.\n\n" +
"Additionally if the ensure file uses $ResolvedVersions directive, checks that " +
"all versions there are up-to-date. Returns non-zero if some version can't be " +
"resolved or $ResolvedVersions file is outdated.",
Advanced: true,
CommandRun: func() subcommands.CommandRun {
c := &ensureFileVerifyRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.ensureFileOptions.registerFlags(&c.Flags, withoutEnsureOutFlag, withoutLegacyListFlag)
return c
},
}
}
type ensureFileVerifyRun struct {
cipdSubcommand
clientOptions
ensureFileOptions
}
func (c *ensureFileVerifyRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 0, 0) {
return 1
}
ctx := cli.GetContext(a, c, env)
ef, err := c.loadEnsureFile(ctx, &c.clientOptions, requireVerifyPlatforms, ignoreVersionsFile)
if err != nil {
return c.done(nil, err)
}
// Resolving all versions in the ensure file also naturally verifies all
// versions exist.
pinMap, versions, err := resolveEnsureFile(ctx, ef, c.clientOptions)
if err != nil || ef.ResolvedVersions == "" {
return c.doneWithPinMap(pinMap, err)
}
// Verify $ResolvedVersions file is up-to-date too.
switch existing, err := loadVersionsFile(ef.ResolvedVersions, c.ensureFile); {
case err != nil:
return c.done(nil, err)
case !existing.Equal(versions):
return c.done(nil, fmt.Errorf("the resolved versions file %s is stale, "+
"use 'cipd ensure-file-resolve -ensure-file %q' to update it",
filepath.Base(ef.ResolvedVersions), c.ensureFile))
default:
return c.doneWithPinMap(pinMap, err)
}
}
////////////////////////////////////////////////////////////////////////////////
// 'ensure-file-resolve' subcommand.
func cmdEnsureFileResolve(params Parameters) *subcommands.Command {
return &subcommands.Command{
UsageLine: "ensure-file-resolve [options]",
ShortDesc: "resolves versions of all packages and writes them into $ResolvedVersions file",
LongDesc: "Resolves versions of all packages for all verified platforms in the \"ensure\" file.\n\n" +
`Writes them to a file specified by $ResolvedVersions directive in the ensure file, ` +
`to be used for version resolution during "cipd ensure ..." instead of calling the backend.`,
Advanced: true,
CommandRun: func() subcommands.CommandRun {
c := &ensureFileResolveRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.ensureFileOptions.registerFlags(&c.Flags, withoutEnsureOutFlag, withoutLegacyListFlag)
return c
},
}
}
type ensureFileResolveRun struct {
cipdSubcommand
clientOptions
ensureFileOptions
}
func (c *ensureFileResolveRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 0, 0) {
return 1
}
ctx := cli.GetContext(a, c, env)
ef, err := c.loadEnsureFile(ctx, &c.clientOptions, requireVerifyPlatforms, ignoreVersionsFile)
switch {
case err != nil:
return c.done(nil, err)
case ef.ResolvedVersions == "":
logging.Errorf(ctx,
"The ensure file doesn't have $ResolvedVersion directive that specifies "+
"where to put the resolved package versions, so it can't be resolved.")
return c.done(nil, errors.New("no resolved versions file configured"))
}
pinMap, versions, err := resolveEnsureFile(ctx, ef, c.clientOptions)
if err != nil {
return c.doneWithPinMap(pinMap, err)
}
if err := saveVersionsFile(ef.ResolvedVersions, versions); err != nil {
return c.done(nil, err)
}
fmt.Printf("The resolved versions have been written to %s.\n\n", filepath.Base(ef.ResolvedVersions))
return c.doneWithPinMap(pinMap, nil)
}
////////////////////////////////////////////////////////////////////////////////
// 'puppet-check-updates' subcommand.
func cmdPuppetCheckUpdates(params Parameters) *subcommands.Command {
return &subcommands.Command{
Advanced: true,
UsageLine: "puppet-check-updates [options]",
ShortDesc: "returns 0 exit code iff 'ensure' will do some actions",
LongDesc: "Returns 0 exit code iff 'ensure' will do some actions.\n\n" +
"Exists to be used from Puppet's Exec 'onlyif' option to trigger " +
"'ensure' only if something is out of date. If puppet-check-updates " +
"fails with a transient error, it returns non-zero exit code (as usual), " +
"so that Puppet doesn't trigger notification chain (that can result in " +
"service restarts). On fatal errors it returns 0 to let Puppet run " +
"'ensure' for real and catch an error.",
CommandRun: func() subcommands.CommandRun {
c := &checkUpdatesRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withRootDir, withMaxThreads)
c.ensureFileOptions.registerFlags(&c.Flags, withoutEnsureOutFlag, withLegacyListFlag)
return c
},
}
}
type checkUpdatesRun struct {
cipdSubcommand
clientOptions
ensureFileOptions
}
func (c *checkUpdatesRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 0, 0) {
return 1
}
ctx := cli.GetContext(a, c, env)
ef, err := c.loadEnsureFile(ctx, &c.clientOptions, ignoreVerifyPlatforms, parseVersionsFile)
if err != nil {
return 0 // on fatal errors ask puppet to run 'ensure' for real
}
_, actions, err := ensurePackages(ctx, ef, "", true, c.clientOptions)
if err != nil {
ret := c.done(actions, err)
if transient.Tag.In(err) {
return ret // fail as usual
}
return 0 // on fatal errors ask puppet to run 'ensure' for real
}
c.done(actions, nil)
if len(actions) == 0 {
return 5 // some arbitrary non-zero number, unlikely to show up on errors
}
return 0
}
////////////////////////////////////////////////////////////////////////////////
// 'resolve' subcommand.
func cmdResolve(params Parameters) *subcommands.Command {
return &subcommands.Command{
UsageLine: "resolve <package or package prefix> [options]",
ShortDesc: "returns concrete package instance ID given a version",
LongDesc: "Returns concrete package instance ID given a version.",
CommandRun: func() subcommands.CommandRun {
c := &resolveRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.Flags.StringVar(&c.version, "version", "<version>", "Package version to resolve.")
return c
},
}
}
type resolveRun struct {
cipdSubcommand
clientOptions
version string
}
func (c *resolveRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 1, 1) {
return 1
}
ctx := cli.GetContext(a, c, env)
return c.doneWithPins(resolveVersion(ctx, args[0], c.version, c.clientOptions))
}
func resolveVersion(ctx context.Context, packagePrefix, version string, clientOpts clientOptions) ([]pinInfo, error) {
packagePrefix, err := expandTemplate(packagePrefix)
if err != nil {
return nil, err
}
client, err := clientOpts.makeCIPDClient(ctx)
if err != nil {
return nil, err
}
defer client.Close(ctx)
return performBatchOperation(ctx, batchOperation{
client: client,
packagePrefix: packagePrefix,
callback: func(pkg string) (common.Pin, error) {
return client.ResolveVersion(ctx, pkg, version)
},
})
}
////////////////////////////////////////////////////////////////////////////////
// 'describe' subcommand.
func cmdDescribe(params Parameters) *subcommands.Command {
return &subcommands.Command{
UsageLine: "describe <package> [options]",
ShortDesc: "returns information about a package instance given its version",
LongDesc: "Returns information about a package instance given its version: " +
"who uploaded the instance and when and a list of attached tags.",
CommandRun: func() subcommands.CommandRun {
c := &describeRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.Flags.StringVar(&c.version, "version", "<version>", "Package version to describe.")
return c
},
}
}
type describeRun struct {
cipdSubcommand
clientOptions
version string
}
func (c *describeRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 1, 1) {
return 1
}
ctx := cli.GetContext(a, c, env)
return c.done(describeInstance(ctx, args[0], c.version, c.clientOptions))
}
func describeInstance(ctx context.Context, pkg, version string, clientOpts clientOptions) (*cipd.InstanceDescription, error) {
pkg, err := expandTemplate(pkg)
if err != nil {
return nil, err
}
client, err := clientOpts.makeCIPDClient(ctx)
if err != nil {
return nil, err
}
defer client.Close(ctx)
pin, err := client.ResolveVersion(ctx, pkg, version)
if err != nil {
return nil, err
}
desc, err := client.DescribeInstance(ctx, pin, &cipd.DescribeInstanceOpts{
DescribeRefs: true,
DescribeTags: true,
})
if err != nil {
return nil, err
}
fmt.Printf("Package: %s\n", desc.Pin.PackageName)
fmt.Printf("Instance ID: %s\n", desc.Pin.InstanceID)
fmt.Printf("Registered by: %s\n", desc.RegisteredBy)
fmt.Printf("Registered at: %s\n", time.Time(desc.RegisteredTs).Local())
if len(desc.Refs) != 0 {
fmt.Printf("Refs:\n")
for _, t := range desc.Refs {
fmt.Printf(" %s\n", t.Ref)
}
} else {
fmt.Printf("Refs: none\n")
}
if len(desc.Tags) != 0 {
fmt.Printf("Tags:\n")
for _, t := range desc.Tags {
fmt.Printf(" %s\n", t.Tag)
}
} else {
fmt.Printf("Tags: none\n")
}
return desc, nil
}
////////////////////////////////////////////////////////////////////////////////
// 'instances' subcommand.
func cmdInstances(params Parameters) *subcommands.Command {
return &subcommands.Command{
UsageLine: "instances <package> [-limit ...]",
ShortDesc: "lists instances of a package",
LongDesc: "Lists instances of a package, most recently uploaded first.",
CommandRun: func() subcommands.CommandRun {
c := &instancesRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.Flags.IntVar(&c.limit, "limit", 20, "How many instances to return or 0 for all.")
return c
},
}
}
type instancesRun struct {
cipdSubcommand
clientOptions
limit int
}
func (c *instancesRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 1, 1) {
return 1
}
ctx := cli.GetContext(a, c, env)
return c.done(listInstances(ctx, args[0], c.limit, c.clientOptions))
}
func listInstances(ctx context.Context, pkg string, limit int, clientOpts clientOptions) (*instancesOutput, error) {
pkg, err := expandTemplate(pkg)
if err != nil {
return nil, err
}
client, err := clientOpts.makeCIPDClient(ctx)
if err != nil {
return nil, err
}
defer client.Close(ctx)
// TODO(vadimsh): The backend currently doesn't support retrieving
// per-instance refs when listing instances. Instead we fetch ALL refs in
// parallel and then merge this information with the instance listing. This
// works fine for packages with few refs (up to 50 maybe), but horribly
// inefficient if the cardinality of the set of all refs is larger than a
// typical size of instance listing (we spend time fetching data we don't
// need). To support this case better, the backend should learn to maintain
// {instance ID => ref} mapping (in addition to {ref => instance ID} mapping
// it already has). This would require back filling all existing entities.
// Fetch the refs in parallel with the first page of results. We merge them
// with the list of instances during the display.
type refsMap map[string][]string // instance ID => list of refs
type refsOrErr struct {
refs refsMap
err error
}
refsChan := make(chan refsOrErr, 1)
go func() {
defer close(refsChan)
asMap := refsMap{}
refs, err := client.FetchPackageRefs(ctx, pkg)
for _, info := range refs {
asMap[info.InstanceID] = append(asMap[info.InstanceID], info.Ref)
}
refsChan <- refsOrErr{asMap, err}
}()
enum, err := client.ListInstances(ctx, pkg)
if err != nil {
return nil, err
}
formatRow := func(instanceID, when, who, refs string) string {
if len(who) > 25 {
who = who[:22] + "..."
}
return fmt.Sprintf("%-44s │ %-21s │ %-25s │ %-12s", instanceID, when, who, refs)
}
var refs refsMap // populated on after fetching first page
out := []instanceInfoWithRefs{}
for {
pageSize := 200
if limit != 0 && limit-len(out) < pageSize {
pageSize = limit - len(out)
if pageSize == 0 {
// Fetched everything we wanted. There's likely more instances available
// (unless '-limit' happens to exactly match number of instances on the
// backend, which is not very probable). Hint this by printing '...'.
fmt.Println(formatRow("...", "...", "...", "..."))
break
}
}
page, err := enum.Next(ctx, pageSize)
if err != nil {
return nil, err
}
if len(page) == 0 {
if len(out) == 0 {
fmt.Println("No instances found")
}
break // no more results to fetch
}
if len(out) == 0 {
// Need to wait for refs to be fetched, they are required to display
// "Refs" column.
refsOrErr := <-refsChan
if refsOrErr.err != nil {
return nil, refsOrErr.err
}
refs = refsOrErr.refs
// Draw the header now that we have some real results (i.e no errors).
hdr := formatRow("Instance ID", "Timestamp", "Uploader", "Refs")
fmt.Println(hdr)
fmt.Println(strings.Repeat("─", len(hdr)))
}
for _, info := range page {
instanceRefs := refs[info.Pin.InstanceID]
out = append(out, instanceInfoWithRefs{
InstanceInfo: info,
Refs: instanceRefs,
})
fmt.Println(formatRow(
info.Pin.InstanceID,
time.Time(info.RegisteredTs).Local().Format("Jan 02 15:04 MST 2006"),
strings.TrimPrefix(info.RegisteredBy, "user:"),
strings.Join(instanceRefs, " ")))
}
}
return &instancesOutput{out}, nil
}
////////////////////////////////////////////////////////////////////////////////
// 'set-ref' subcommand.
func cmdSetRef(params Parameters) *subcommands.Command {
return &subcommands.Command{
UsageLine: "set-ref <package or package prefix> [options]",
ShortDesc: "moves a ref to point to a given version",
LongDesc: "Moves a ref to point to a given version.",
CommandRun: func() subcommands.CommandRun {
c := &setRefRun{}
c.registerBaseFlags()
c.refsOptions.registerFlags(&c.Flags)
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.Flags.StringVar(&c.version, "version", "<version>", "Package version to point the ref to.")
return c
},
}
}
type setRefRun struct {
cipdSubcommand
refsOptions
clientOptions
version string
}
func (c *setRefRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 1, 1) {
return 1
}
if len(c.refs) == 0 {
return c.done(nil, makeCLIError("at least one -ref must be provided"))
}
pkgPrefix, err := expandTemplate(args[0])
if err != nil {
return c.done(nil, err)
}
ctx := cli.GetContext(a, c, env)
return c.doneWithPins(visitPins(ctx, &visitPinsArgs{
clientOptions: c.clientOptions,
packagePrefix: pkgPrefix,
version: c.version,
updatePin: func(client cipd.Client, pin common.Pin) error {
for _, ref := range c.refs {
if err := client.SetRefWhenReady(ctx, ref, pin); err != nil {
return err
}
}
return nil
},
}))
}
type visitPinsArgs struct {
clientOptions
packagePrefix string
version string
updatePin func(client cipd.Client, pin common.Pin) error
}
func visitPins(ctx context.Context, args *visitPinsArgs) ([]pinInfo, error) {
client, err := args.clientOptions.makeCIPDClient(ctx)
if err != nil {
return nil, err
}
defer client.Close(ctx)
client.BeginBatch(ctx)
defer client.EndBatch(ctx)
// Do not touch anything if some packages do not have requested version. So
// resolve versions first and only then move refs.
pins, err := performBatchOperation(ctx, batchOperation{
client: client,
packagePrefix: args.packagePrefix,
callback: func(pkg string) (common.Pin, error) {
return client.ResolveVersion(ctx, pkg, args.version)
},
})
if err != nil {
return nil, err
}
if hasErrors(pins) {
printPinsAndError(map[string][]pinInfo{"": pins})
return nil, fmt.Errorf("can't find %q version in all packages, aborting", args.version)
}
// Prepare for the next batch call.
packages := make([]string, len(pins))
pinsToUse := make(map[string]common.Pin, len(pins))
for i, p := range pins {
packages[i] = p.Pkg
pinsToUse[p.Pkg] = *p.Pin
}
// Update all refs or tags.
return performBatchOperation(ctx, batchOperation{
client: client,
packages: packages,
callback: func(pkg string) (common.Pin, error) {
pin := pinsToUse[pkg]
if err := args.updatePin(client, pin); err != nil {
return common.Pin{}, err
}
return pin, nil
},
})
}
////////////////////////////////////////////////////////////////////////////////
// 'set-tag' subcommand.
func cmdSetTag(params Parameters) *subcommands.Command {
return &subcommands.Command{
UsageLine: "set-tag <package or package prefix> -tag key:value [options]",
ShortDesc: "tags package of a specific version",
LongDesc: "Tags package of a specific version",
CommandRun: func() subcommands.CommandRun {
c := &setTagRun{}
c.registerBaseFlags()
c.tagsOptions.registerFlags(&c.Flags)
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.Flags.StringVar(&c.version, "version", "<version>",
"Package version to resolve. Could also be a tag or a ref.")
return c
},
}
}
type setTagRun struct {
cipdSubcommand
tagsOptions
clientOptions
version string
}
func (c *setTagRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 1, 1) {
return 1
}
if len(c.tags) == 0 {
return c.done(nil, makeCLIError("at least one -tag must be provided"))
}
pkgPrefix, err := expandTemplate(args[0])
if err != nil {
return c.done(nil, err)
}
ctx := cli.GetContext(a, c, env)
return c.done(visitPins(ctx, &visitPinsArgs{
clientOptions: c.clientOptions,
packagePrefix: pkgPrefix,
version: c.version,
updatePin: func(client cipd.Client, pin common.Pin) error {
return client.AttachTagsWhenReady(ctx, pin, c.tags)
},
}))
}
////////////////////////////////////////////////////////////////////////////////
// 'set-metadata' subcommand.
func cmdSetMetadata(params Parameters) *subcommands.Command {
return &subcommands.Command{
UsageLine: "set-metadata <package or package prefix> -metadata key:value -metadata-from-file key:path [options]",
ShortDesc: "attaches metadata to an instance",
LongDesc: "Attaches metadata to an instance",
CommandRun: func() subcommands.CommandRun {
c := &setMetadataRun{}
c.registerBaseFlags()
c.metadataOptions.registerFlags(&c.Flags)
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.Flags.StringVar(&c.version, "version", "<version>",
"Package version to resolve. Could also be a tag or a ref.")
return c
},
}
}
type setMetadataRun struct {
cipdSubcommand
metadataOptions
clientOptions
version string
}
func (c *setMetadataRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 1, 1) {
return 1
}
ctx := cli.GetContext(a, c, env)
md, err := c.metadataOptions.load(ctx)
if err == nil && len(md) == 0 {
err = makeCLIError("at least one -metadata or -metadata-from-file must be provided")
}
if err != nil {
return c.done(nil, err)
}
pkgPrefix, err := expandTemplate(args[0])
if err != nil {
return c.done(nil, err)
}
return c.doneWithPins(visitPins(ctx, &visitPinsArgs{
clientOptions: c.clientOptions,
packagePrefix: pkgPrefix,
version: c.version,
updatePin: func(client cipd.Client, pin common.Pin) error {
return client.AttachMetadataWhenReady(ctx, pin, md)
},
}))
}
////////////////////////////////////////////////////////////////////////////////
// 'ls' subcommand.
func cmdListPackages(params Parameters) *subcommands.Command {
return &subcommands.Command{
UsageLine: "ls [-r] [<prefix string>]",
ShortDesc: "lists matching packages on the server",
LongDesc: "Queries the backend for a list of packages in the given path to " +
"which the user has access, optionally recursively.",
CommandRun: func() subcommands.CommandRun {
c := &listPackagesRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.Flags.BoolVar(&c.recursive, "r", false, "Whether to list packages in subdirectories.")
c.Flags.BoolVar(&c.showHidden, "h", false, "Whether also to list hidden packages.")
return c
},
}
}
type listPackagesRun struct {
cipdSubcommand
clientOptions
recursive bool
showHidden bool
}
func (c *listPackagesRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 0, 1) {
return 1
}
path, err := "", error(nil)
if len(args) == 1 {
path, err = expandTemplate(args[0])
if err != nil {
return c.done(nil, err)
}
}
ctx := cli.GetContext(a, c, env)
return c.done(listPackages(ctx, path, c.recursive, c.showHidden, c.clientOptions))
}
func listPackages(ctx context.Context, path string, recursive, showHidden bool, clientOpts clientOptions) ([]string, error) {
client, err := clientOpts.makeCIPDClient(ctx)
if err != nil {
return nil, err
}
defer client.Close(ctx)
packages, err := client.ListPackages(ctx, path, recursive, showHidden)
if err != nil {
return nil, err
}
if len(packages) == 0 {
fmt.Println("No matching packages.")
} else {
for _, p := range packages {
fmt.Println(p)
}
}
return packages, nil
}
////////////////////////////////////////////////////////////////////////////////
// 'search' subcommand.
func cmdSearch(params Parameters) *subcommands.Command {
return &subcommands.Command{
UsageLine: "search <package> -tag key:value [options]",
ShortDesc: "searches for package instances by tag",
LongDesc: "Searches for instances of some package with all given tags.",
CommandRun: func() subcommands.CommandRun {
c := &searchRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.tagsOptions.registerFlags(&c.Flags)
return c
},
}
}
type searchRun struct {
cipdSubcommand
clientOptions
tagsOptions
}
func (c *searchRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 1, 1) {
return 1
}
if len(c.tags) == 0 {
return c.done(nil, makeCLIError("at least one -tag must be provided"))
}
packageName, err := expandTemplate(args[0])
if err != nil {
return c.done(nil, err)
}
ctx := cli.GetContext(a, c, env)
return c.done(searchInstances(ctx, packageName, c.tags, c.clientOptions))
}
func searchInstances(ctx context.Context, packageName string, tags []string, clientOpts clientOptions) ([]common.Pin, error) {
client, err := clientOpts.makeCIPDClient(ctx)
if err != nil {
return nil, err
}
defer client.Close(ctx)
pins, err := client.SearchInstances(ctx, packageName, tags)
if err != nil {
return nil, err
}
if len(pins) == 0 {
fmt.Println("No matching instances.")
} else {
fmt.Println("Instances:")
for _, pin := range pins {
fmt.Printf(" %s\n", pin)
}
}
return pins, err
}
////////////////////////////////////////////////////////////////////////////////
// 'acl-list' subcommand.
func cmdListACL(params Parameters) *subcommands.Command {
return &subcommands.Command{
Advanced: true,
UsageLine: "acl-list <package subpath>",
ShortDesc: "lists package path Access Control List",
LongDesc: "Lists package path Access Control List.",
CommandRun: func() subcommands.CommandRun {
c := &listACLRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
return c
},
}
}
type listACLRun struct {
cipdSubcommand
clientOptions
}
func (c *listACLRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 1, 1) {
return 1
}
pkg, err := expandTemplate(args[0])
if err != nil {
return c.done(nil, err)
}
ctx := cli.GetContext(a, c, env)
return c.done(listACL(ctx, pkg, c.clientOptions))
}
func listACL(ctx context.Context, packagePath string, clientOpts clientOptions) (map[string][]cipd.PackageACL, error) {
client, err := clientOpts.makeCIPDClient(ctx)
if err != nil {
return nil, err
}
defer client.Close(ctx)
acls, err := client.FetchACL(ctx, packagePath)
if err != nil {
return nil, err
}
// Split by role, drop empty ACLs.
byRole := map[string][]cipd.PackageACL{}
for _, a := range acls {
if len(a.Principals) != 0 {
byRole[a.Role] = append(byRole[a.Role], a)
}
}
listRoleACL := func(title string, acls []cipd.PackageACL) {
fmt.Printf("%s:\n", title)
if len(acls) == 0 {
fmt.Printf(" none\n")
return
}
for _, a := range acls {
fmt.Printf(" via %q:\n", a.PackagePath)
for _, u := range a.Principals {
fmt.Printf(" %s\n", u)
}
}
}
listRoleACL("Owners", byRole["OWNER"])
listRoleACL("Writers", byRole["WRITER"])
listRoleACL("Readers", byRole["READER"])
return byRole, nil
}
////////////////////////////////////////////////////////////////////////////////
// 'acl-edit' subcommand.
// principalsList is used as custom flag value. It implements flag.Value.
type principalsList []string
func (l *principalsList) String() string {
return fmt.Sprintf("%v", *l)
}
func (l *principalsList) Set(value string) error {
// Ensure <type>:<id> syntax is used. Let the backend to validate the rest.
chunks := strings.Split(value, ":")
if len(chunks) != 2 {
return makeCLIError("%q doesn't look like principal id (<type>:<id>)", value)
}
*l = append(*l, value)
return nil
}
func cmdEditACL(params Parameters) *subcommands.Command {
return &subcommands.Command{
Advanced: true,
UsageLine: "acl-edit <package subpath> [options]",
ShortDesc: "modifies package path Access Control List",
LongDesc: "Modifies package path Access Control List.",
CommandRun: func() subcommands.CommandRun {
c := &editACLRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.Flags.Var(&c.owner, "owner", "Users (user:email) or groups (`group:name`) to grant OWNER role.")
c.Flags.Var(&c.writer, "writer", "Users (user:email) or groups (`group:name`) to grant WRITER role.")
c.Flags.Var(&c.reader, "reader", "Users (user:email) or groups (`group:name`) to grant READER role.")
c.Flags.Var(&c.revoke, "revoke", "Users (user:email) or groups (`group:name`) to remove from all roles.")
return c
},
}
}
type editACLRun struct {
cipdSubcommand
clientOptions
owner principalsList
writer principalsList
reader principalsList
revoke principalsList
}
func (c *editACLRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 1, 1) {
return 1
}
pkg, err := expandTemplate(args[0])
if err != nil {
return c.done(nil, err)
}
ctx := cli.GetContext(a, c, env)
return c.done(nil, editACL(ctx, pkg, c.owner, c.writer, c.reader, c.revoke, c.clientOptions))
}
func editACL(ctx context.Context, packagePath string, owners, writers, readers, revoke principalsList, clientOpts clientOptions) error {
changes := []cipd.PackageACLChange{}
makeChanges := func(action cipd.PackageACLChangeAction, role string, list principalsList) {
for _, p := range list {
changes = append(changes, cipd.PackageACLChange{
Action: action,
Role: role,
Principal: p,
})
}
}
makeChanges(cipd.GrantRole, "OWNER", owners)
makeChanges(cipd.GrantRole, "WRITER", writers)
makeChanges(cipd.GrantRole, "READER", readers)
makeChanges(cipd.RevokeRole, "OWNER", revoke)
makeChanges(cipd.RevokeRole, "WRITER", revoke)
makeChanges(cipd.RevokeRole, "READER", revoke)
if len(changes) == 0 {
return nil
}
client, err := clientOpts.makeCIPDClient(ctx)
if err != nil {
return err
}
defer client.Close(ctx)
err = client.ModifyACL(ctx, packagePath, changes)
if err != nil {
return err
}
fmt.Println("ACL changes applied.")
return nil
}
////////////////////////////////////////////////////////////////////////////////
// 'acl-check' subcommand.
func cmdCheckACL(params Parameters) *subcommands.Command {
return &subcommands.Command{
Advanced: true,
UsageLine: "acl-check <package subpath> [options]",
ShortDesc: "checks whether the caller has given roles in a package",
LongDesc: "Checks whether the caller has given roles in a package.",
CommandRun: func() subcommands.CommandRun {
c := &checkACLRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.Flags.BoolVar(&c.owner, "owner", false, "Check for OWNER role.")
c.Flags.BoolVar(&c.writer, "writer", false, "Check for WRITER role.")
c.Flags.BoolVar(&c.reader, "reader", false, "Check for READER role.")
return c
},
}
}
type checkACLRun struct {
cipdSubcommand
clientOptions
owner bool
writer bool
reader bool
}
func (c *checkACLRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 1, 1) {
return 1
}
var roles []string
if c.owner {
roles = append(roles, "OWNER")
}
if c.writer {
roles = append(roles, "WRITER")
}
if c.reader {
roles = append(roles, "READER")
}
// By default, check for READER access.
if len(roles) == 0 {
roles = append(roles, "READER")
}
pkg, err := expandTemplate(args[0])
if err != nil {
return c.done(nil, err)
}
ctx := cli.GetContext(a, c, env)
return c.done(checkACL(ctx, pkg, roles, c.clientOptions))
}
func checkACL(ctx context.Context, packagePath string, roles []string, clientOpts clientOptions) (bool, error) {
client, err := clientOpts.makeCIPDClient(ctx)
if err != nil {
return false, err
}
defer client.Close(ctx)
actualRoles, err := client.FetchRoles(ctx, packagePath)
if err != nil {
return false, err
}
roleSet := stringset.NewFromSlice(actualRoles...)
var missing []string
for _, r := range roles {
if !roleSet.Has(r) {
missing = append(missing, r)
}
}
if len(missing) == 0 {
fmt.Printf("The caller has all requested role(s): %s\n", strings.Join(roles, ", "))
return true, nil
}
fmt.Printf("The caller doesn't have following role(s): %s\n", strings.Join(missing, ", "))
return false, nil
}
////////////////////////////////////////////////////////////////////////////////
// 'pkg-build' subcommand.
func cmdBuild() *subcommands.Command {
return &subcommands.Command{
Advanced: true,
UsageLine: "pkg-build [options]",
ShortDesc: "builds a package instance file",
LongDesc: "Builds a package instance producing *.cipd file.",
CommandRun: func() subcommands.CommandRun {
c := &buildRun{}
c.registerBaseFlags()
c.inputOptions.registerFlags(&c.Flags)
c.hashOptions.registerFlags(&c.Flags)
c.Flags.StringVar(&c.outputFile, "out", "<path>", "Path to a file to write the final package to.")
return c
},
}
}
type buildRun struct {
cipdSubcommand
inputOptions
hashOptions
outputFile string
}
func (c *buildRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 0, 0) {
return 1
}
ctx := cli.GetContext(a, c, env)
_, err := buildInstanceFile(ctx, c.outputFile, c.inputOptions, c.hashAlgo())
if err != nil {
return c.done(nil, err)
}
return c.done(inspectInstanceFile(ctx, c.outputFile, c.hashAlgo(), false))
}
func buildInstanceFile(ctx context.Context, instanceFile string, inputOpts inputOptions, algo api.HashAlgo) (common.Pin, error) {
// Read the list of files to add to the package.
buildOpts, err := inputOpts.prepareInput()
if err != nil {
return common.Pin{}, err
}
// Prepare the destination, update build options with io.Writer to it.
out, err := os.OpenFile(instanceFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
return common.Pin{}, err
}
buildOpts.Output = out
buildOpts.HashAlgo = algo
// Build the package.
pin, err := builder.BuildInstance(ctx, buildOpts)
if err != nil {
out.Close()
os.Remove(instanceFile)
return common.Pin{}, err
}
// Make sure it is flushed properly by ensuring Close succeeds.
if err := out.Close(); err != nil {
return common.Pin{}, err
}
return pin, nil
}
////////////////////////////////////////////////////////////////////////////////
// 'pkg-deploy' subcommand.
func cmdDeploy() *subcommands.Command {
return &subcommands.Command{
Advanced: true,
UsageLine: "pkg-deploy <package instance file> [options]",
ShortDesc: "deploys a package instance file",
LongDesc: "Deploys a *.cipd package instance into a site root.",
CommandRun: func() subcommands.CommandRun {
c := &deployRun{}
c.registerBaseFlags()
c.hashOptions.registerFlags(&c.Flags)
c.maxThreadsOption.registerFlags(&c.Flags)
c.Flags.StringVar(&c.rootDir, "root", "<path>", "Path to an installation site root directory.")
return c
},
}
}
type deployRun struct {
cipdSubcommand
hashOptions
maxThreadsOption
rootDir string
}
func (c *deployRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 1, 1) {
return 1
}
ctx := cli.GetContext(a, c, env)
maxThreads, err := c.loadMaxThreads(ctx)
if err != nil {
return c.done(nil, err)
}
return c.done(deployInstanceFile(ctx, c.rootDir, args[0], c.hashAlgo(), maxThreads))
}
func deployInstanceFile(ctx context.Context, root, instanceFile string, hashAlgo api.HashAlgo, maxThreads int) (common.Pin, error) {
inst, err := reader.OpenInstanceFile(ctx, instanceFile, reader.OpenInstanceOpts{
VerificationMode: reader.CalculateHash,
HashAlgo: hashAlgo,
})
if err != nil {
return common.Pin{}, err
}
defer inst.Close(ctx, false)
inspectInstance(ctx, inst, false)
d := deployer.New(root)
defer d.FS().CleanupTrash(ctx)
// TODO(iannucci): add subdir arg to deployRun
return d.DeployInstance(ctx, "", inst, maxThreads)
}
////////////////////////////////////////////////////////////////////////////////
// 'pkg-fetch' subcommand.
func cmdFetch(params Parameters) *subcommands.Command {
return &subcommands.Command{
Advanced: true,
UsageLine: "pkg-fetch <package> [options]",
ShortDesc: "fetches a package instance file from the repository",
LongDesc: "Fetches a package instance file from the repository.",
CommandRun: func() subcommands.CommandRun {
c := &fetchRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.Flags.StringVar(&c.version, "version", "<version>", "Package version to fetch.")
c.Flags.StringVar(&c.outputPath, "out", "<path>", "Path to a file to write fetch to.")
return c
},
}
}
type fetchRun struct {
cipdSubcommand
clientOptions
version string
outputPath string
}
func (c *fetchRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 1, 1) {
return 1
}
pkg, err := expandTemplate(args[0])
if err != nil {
return c.done(nil, err)
}
ctx := cli.GetContext(a, c, env)
return c.done(fetchInstanceFile(ctx, pkg, c.version, c.outputPath, c.clientOptions))
}
func fetchInstanceFile(ctx context.Context, packageName, version, instanceFile string, clientOpts clientOptions) (common.Pin, error) {
client, err := clientOpts.makeCIPDClient(ctx)
if err != nil {
return common.Pin{}, err
}
defer client.Close(ctx)
pin, err := client.ResolveVersion(ctx, packageName, version)
if err != nil {
return common.Pin{}, err
}
out, err := os.OpenFile(instanceFile, os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
return common.Pin{}, err
}
ok := false
defer func() {
if !ok {
out.Close()
os.Remove(instanceFile)
}
}()
err = client.FetchInstanceTo(ctx, pin, out)
if err != nil {
return common.Pin{}, err
}
out.Close()
ok = true
// Print information about the instance. 'FetchInstanceTo' already verified
// the hash.
inst, err := reader.OpenInstanceFile(ctx, instanceFile, reader.OpenInstanceOpts{
VerificationMode: reader.SkipHashVerification,
InstanceID: pin.InstanceID,
})
if err != nil {
os.Remove(instanceFile)
return common.Pin{}, err
}
defer inst.Close(ctx, false)
inspectInstance(ctx, inst, false)
return inst.Pin(), nil
}
////////////////////////////////////////////////////////////////////////////////
// 'pkg-inspect' subcommand.
func cmdInspect() *subcommands.Command {
return &subcommands.Command{
Advanced: true,
UsageLine: "pkg-inspect <package instance file>",
ShortDesc: "inspects contents of a package instance file",
LongDesc: "Reads contents *.cipd file and prints information about it.",
CommandRun: func() subcommands.CommandRun {
c := &inspectRun{}
c.registerBaseFlags()
c.hashOptions.registerFlags(&c.Flags)
return c
},
}
}
type inspectRun struct {
cipdSubcommand
hashOptions
}
func (c *inspectRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 1, 1) {
return 1
}
ctx := cli.GetContext(a, c, env)
return c.done(inspectInstanceFile(ctx, args[0], c.hashAlgo(), true))
}
func inspectInstanceFile(ctx context.Context, instanceFile string, hashAlgo api.HashAlgo, listFiles bool) (common.Pin, error) {
inst, err := reader.OpenInstanceFile(ctx, instanceFile, reader.OpenInstanceOpts{
VerificationMode: reader.CalculateHash,
HashAlgo: hashAlgo,
})
if err != nil {
return common.Pin{}, err
}
defer inst.Close(ctx, false)
inspectInstance(ctx, inst, listFiles)
return inst.Pin(), nil
}
func inspectPin(ctx context.Context, pin common.Pin) {
fmt.Printf("Instance: %s\n", pin)
}
func inspectInstance(ctx context.Context, inst pkg.Instance, listFiles bool) {
inspectPin(ctx, inst.Pin())
if listFiles {
fmt.Println("Package files:")
for _, f := range inst.Files() {
if f.Symlink() {
target, err := f.SymlinkTarget()
if err != nil {
fmt.Printf(" E %s (%s)\n", f.Name(), err)
} else {
fmt.Printf(" S %s -> %s\n", f.Name(), target)
}
} else {
flags := make([]string, 0, 3)
if f.Executable() {
flags = append(flags, "+x")
}
if f.WinAttrs()&fs.WinAttrHidden != 0 {
flags = append(flags, "+H")
}
if f.WinAttrs()&fs.WinAttrSystem != 0 {
flags = append(flags, "+S")
}
flagText := ""
if len(flags) > 0 {
flagText = fmt.Sprintf(" (%s)", strings.Join(flags, ""))
}
fmt.Printf(" F %s%s\n", f.Name(), flagText)
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
// 'pkg-register' subcommand.
func cmdRegister(params Parameters) *subcommands.Command {
return &subcommands.Command{
Advanced: true,
UsageLine: "pkg-register <package instance file>",
ShortDesc: "uploads and registers package instance in the package repository",
LongDesc: "Uploads and registers package instance in the package repository.",
CommandRun: func() subcommands.CommandRun {
c := &registerRun{}
c.registerBaseFlags()
c.Opts.refsOptions.registerFlags(&c.Flags)
c.Opts.tagsOptions.registerFlags(&c.Flags)
c.Opts.metadataOptions.registerFlags(&c.Flags)
c.Opts.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.Opts.uploadOptions.registerFlags(&c.Flags)
c.Opts.hashOptions.registerFlags(&c.Flags)
return c
},
}
}
type registerOpts struct {
refsOptions
tagsOptions
metadataOptions
clientOptions
uploadOptions
hashOptions
}
type registerRun struct {
cipdSubcommand
Opts registerOpts
}
func (c *registerRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 1, 1) {
return 1
}
ctx := cli.GetContext(a, c, env)
return c.done(registerInstanceFile(ctx, args[0], nil, &c.Opts))
}
func registerInstanceFile(ctx context.Context, instanceFile string, knownPin *common.Pin, opts *registerOpts) (common.Pin, error) {
// Load metadata, in particular process -metadata-from-file, which may fail.
metadata, err := opts.metadataOptions.load(ctx)
if err != nil {
return common.Pin{}, err
}
src, err := pkg.NewFileSource(instanceFile)
if err != nil {
return common.Pin{}, err
}
defer src.Close(ctx, false)
// Calculate the pin if not yet known.
var pin common.Pin
if knownPin != nil {
pin = *knownPin
} else {
pin, err = reader.CalculatePin(ctx, src, opts.hashAlgo())
if err != nil {
return common.Pin{}, err
}
}
inspectPin(ctx, pin)
client, err := opts.clientOptions.makeCIPDClient(ctx)
if err != nil {
return common.Pin{}, err
}
defer client.Close(ctx)
err = client.RegisterInstance(ctx, pin, src, opts.uploadOptions.verificationTimeout)
if err != nil {
return common.Pin{}, err
}
err = attachAndMove(ctx, client, pin, metadata, opts.tags, opts.refs)
if err != nil {
return common.Pin{}, err
}
return pin, nil
}
func attachAndMove(ctx context.Context, client cipd.Client, pin common.Pin, md []cipd.Metadata, tags tagList, refs refList) error {
if err := client.AttachMetadataWhenReady(ctx, pin, md); err != nil {
return err
}
if err := client.AttachTagsWhenReady(ctx, pin, tags); err != nil {
return err
}
for _, ref := range refs {
if err := client.SetRefWhenReady(ctx, ref, pin); err != nil {
return err
}
}
return nil
}
////////////////////////////////////////////////////////////////////////////////
// 'selfupdate' subcommand.
func cmdSelfUpdate(params Parameters) *subcommands.Command {
return &subcommands.Command{
UsageLine: "selfupdate -version <version> | -version-file <path>",
ShortDesc: "updates the current CIPD client binary",
LongDesc: "Does an in-place upgrade to the current CIPD binary.\n\n" +
"Reads the version either from the command line (when using -version) or " +
"from a file (when using -version-file). When using -version-file, also " +
"loads special *.digests file (from <version-file>.digests path) with " +
"pinned hashes of the client binary for all platforms. When selfupdating, " +
"the client will verify the new downloaded binary has a hash specified in " +
"the *.digests file.",
CommandRun: func() subcommands.CommandRun {
c := &selfupdateRun{}
// By default, show a reduced number of logs unless something goes wrong.
c.logConfig.Level = logging.Warning
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.Flags.StringVar(&c.version, "version", "", "Version of the client to update to (incompatible with -version-file).")
c.Flags.StringVar(&c.versionFile, "version-file", "",
"Indicates the path to read the new version from (<version-file> itself) and "+
"the path to the file with pinned hashes of the CIPD binary (<version-file>.digests file).")
return c
},
}
}
type selfupdateRun struct {
cipdSubcommand
clientOptions
version string
versionFile string
}
func (c *selfupdateRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 0, 0) {
return 1
}
ctx := cli.GetContext(a, c, env)
switch {
case c.version != "" && c.versionFile != "":
return c.done(nil, makeCLIError("-version and -version-file are mutually exclusive, use only one"))
case c.version == "" && c.versionFile == "":
return c.done(nil, makeCLIError("either -version or -version-file are required"))
}
var version = c.version
var digests *digests.ClientDigestsFile
if version == "" { // using -version-file instead? load *.digests
var err error
version, err = loadClientVersion(c.versionFile)
if err != nil {
return c.done(nil, err)
}
digests, err = loadClientDigests(c.versionFile + digestsSfx)
if err != nil {
return c.done(nil, err)
}
}
return c.done(func() (common.Pin, error) {
exePath, err := os.Executable()
if err != nil {
return common.Pin{}, err
}
opts, err := c.clientOptions.toCIPDClientOpts(ctx)
if err != nil {
return common.Pin{}, err
}
return cipd.MaybeUpdateClient(ctx, opts, version, exePath, digests)
}())
}
////////////////////////////////////////////////////////////////////////////////
// 'selfupdate-roll' subcommand.
func cmdSelfUpdateRoll(params Parameters) *subcommands.Command {
return &subcommands.Command{
Advanced: true,
UsageLine: "selfupdate-roll -version-file <path> (-version <version> | -check)",
ShortDesc: "generates or checks the client version and *.digests files",
LongDesc: "Generates or checks the client version and *.digests files.\n\n" +
"When -version is specified, takes its value as CIPD client version, " +
"resolves it into a list of hashes of the client binary at this version " +
"for all known platforms, and (on success) puts the version into a file " +
"specified by -version-file (referred to as <version-file> below), and " +
"all hashes into <version-file>.digests file. They are later used by " +
"'selfupdate -version-file <version-file>' to verify validity of the " +
"fetched binary.\n\n" +
"If -version is not specified, reads it from <version-file> and generates " +
"<version-file>.digests file based on it.\n\n" +
"When using -check, just verifies hashes in the <version-file>.digests " +
"file match the version recorded in the <version-file>.",
CommandRun: func() subcommands.CommandRun {
c := &selfupdateRollRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
c.Flags.StringVar(&c.version, "version", "", "Version of the client to roll to.")
c.Flags.StringVar(&c.versionFile, "version-file", "<version-file>",
"Indicates the path to a file with the version (<version-file> itself) and "+
"the path to the file with pinned hashes of the CIPD binary (<version-file>.digests file).")
c.Flags.BoolVar(&c.check, "check", false, "If set, checks that the file with "+
"pinned hashes of the CIPD binary (<version-file>.digests file) is up-to-date.")
return c
},
}
}
type selfupdateRollRun struct {
cipdSubcommand
clientOptions
version string
versionFile string
check bool
}
func (c *selfupdateRollRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 0, 0) {
return 1
}
ctx := cli.GetContext(a, c, env)
client, err := c.clientOptions.makeCIPDClient(ctx)
if err != nil {
return c.done(nil, err)
}
defer client.Close(ctx)
if c.check {
if c.version != "" {
return c.done(nil, makeCLIError("-version should not be used in -check mode"))
}
version, err := loadClientVersion(c.versionFile)
if err != nil {
return c.done(nil, err)
}
return c.doneWithPins(checkClientDigests(ctx, client, c.versionFile+digestsSfx, version))
}
// Grab the version from the command line and fallback to the -version-file
// otherwise. The fallback is useful when we just want to regenerate *.digests
// without touching the version file itself.
version := c.version
if version == "" {
var err error
version, err = loadClientVersion(c.versionFile)
if err != nil {
return c.done(nil, err)
}
}
// It really makes sense to pin only tags. Warn about that. Still proceed,
// maybe users are using refs and do not move them by convention.
switch {
case common.ValidateInstanceID(version, common.AnyHash) == nil:
return c.done(nil, fmt.Errorf("expecting a version identifier that can be "+
"resolved for all per-platform CIPD client packages, not a concrete instance ID"))
case common.ValidateInstanceTag(version) != nil:
fmt.Printf(
"WARNING! Version %q is not a tag. The hash pinning in *.digests file is "+
"only useful for unmovable version identifiers. Proceeding, assuming "+
"the immutability of %q is maintained manually. If it moves, selfupdate "+
"will break due to *.digests file no longer matching the packages!\n\n",
version, version)
}
pins, err := generateClientDigests(ctx, client, c.versionFile+digestsSfx, version)
if err != nil {
return c.doneWithPins(pins, err)
}
if c.version != "" {
if err := ioutil.WriteFile(c.versionFile, []byte(c.version+"\n"), 0666); err != nil {
return c.done(nil, err)
}
}
return c.doneWithPins(pins, nil)
}
////////////////////////////////////////////////////////////////////////////////
const digestsSfx = ".digests"
func generateClientDigests(ctx context.Context, client cipd.Client, path, version string) ([]pinInfo, error) {
digests, pins, err := assembleClientDigests(ctx, client, version)
if err != nil {
return pins, err
}
buf := bytes.Buffer{}
versionFileName := strings.TrimSuffix(filepath.Base(path), digestsSfx)
if err := digests.Serialize(&buf, version, versionFileName); err != nil {
return nil, err
}
if err := ioutil.WriteFile(path, buf.Bytes(), 0666); err != nil {
return nil, err
}
fmt.Printf("The pinned client hashes have been written to %s.\n\n", filepath.Base(path))
return pins, nil
}
func checkClientDigests(ctx context.Context, client cipd.Client, path, version string) ([]pinInfo, error) {
existing, err := loadClientDigests(path)
if err != nil {
return nil, err
}
digests, pins, err := assembleClientDigests(ctx, client, version)
if err != nil {
return pins, err
}
if !digests.Equal(existing) {
base := filepath.Base(path)
return nil, fmt.Errorf("the file with pinned client hashes (%s) is stale, "+
"use 'cipd selfupdate-roll -version-file %s' to update it",
base, strings.TrimSuffix(base, digestsSfx))
}
fmt.Printf("The file with pinned client hashes (%s) is up-to-date.\n\n", filepath.Base(path))
return pins, nil
}
// loadClientVersion reads a version string from a file.
func loadClientVersion(path string) (string, error) {
blob, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
version := strings.TrimSpace(string(blob))
if err := common.ValidateInstanceVersion(version); err != nil {
return "", err
}
return version, nil
}
// loadClientDigests loads the *.digests file with client binary hashes.
func loadClientDigests(path string) (*digests.ClientDigestsFile, error) {
switch f, err := os.Open(path); {
case os.IsNotExist(err):
base := filepath.Base(path)
return nil, fmt.Errorf("the file with pinned client hashes (%s) doesn't exist, "+
"use 'cipd selfupdate-roll -version-file %s' to generate it",
base, strings.TrimSuffix(base, digestsSfx))
case err != nil:
return nil, err
default:
defer f.Close()
return digests.ParseClientDigestsFile(f)
}
}
// assembleClientDigests produces the digests file by making backend RPCs.
func assembleClientDigests(ctx context.Context, c cipd.Client, version string) (*digests.ClientDigestsFile, []pinInfo, error) {
if !strings.HasSuffix(cipd.ClientPackage, "/${platform}") {
panic(fmt.Sprintf("client package template (%q) is expected to end with '${platform}'", cipd.ClientPackage))
}
out := &digests.ClientDigestsFile{}
mu := sync.Mutex{}
// Ask the backend to give us hashes of the client binary for all platforms.
pins, err := performBatchOperation(ctx, batchOperation{
client: c,
packagePrefix: strings.TrimSuffix(cipd.ClientPackage, "${platform}"),
callback: func(pkg string) (common.Pin, error) {
pin, err := c.ResolveVersion(ctx, pkg, version)
if err != nil {
return common.Pin{}, err
}
desc, err := c.DescribeClient(ctx, pin)
if err != nil {
return common.Pin{}, err
}
mu.Lock()
defer mu.Unlock()
plat := pkg[strings.LastIndex(pkg, "/")+1:]
if err := out.AddClientRef(plat, desc.Digest); err != nil {
return common.Pin{}, err
}
return pin, nil
},
})
switch {
case err != nil:
return nil, pins, err
case hasErrors(pins):
return nil, pins, errors.New("failed to obtain the client binary digest for all platforms")
}
out.Sort()
return out, pins, nil
}
////////////////////////////////////////////////////////////////////////////////
// 'deployment-check' subcommand.
func cmdCheckDeployment(params Parameters) *subcommands.Command {
return &subcommands.Command{
Advanced: true,
UsageLine: "deployment-check [options]",
ShortDesc: "verifies all files that are supposed to be installed are present",
LongDesc: "Compares CIPD package manifests stored in .cipd/* with what's on disk.\n\n" +
"Useful when debugging issues with broken installations.",
CommandRun: func() subcommands.CommandRun {
c := &checkDeploymentRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withRootDir, withoutMaxThreads)
return c
},
}
}
type checkDeploymentRun struct {
cipdSubcommand
clientOptions
}
func (c *checkDeploymentRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 0, 0) {
return 1
}
ctx := cli.GetContext(a, c, env)
return c.done(checkDeployment(ctx, c.clientOptions))
}
func checkDeployment(ctx context.Context, clientOpts clientOptions) (cipd.ActionMap, error) {
client, err := clientOpts.makeCIPDClient(ctx)
if err != nil {
return nil, err
}
defer client.Close(ctx)
currentDeployment, err := client.FindDeployed(ctx)
if err != nil {
return nil, err
}
actions, err := client.EnsurePackages(ctx, currentDeployment, &cipd.EnsureOptions{
Paranoia: cipd.CheckIntegrity,
DryRun: true,
Silent: true,
})
if err != nil {
return nil, err
}
actions.Log(ctx, true)
if len(actions) != 0 {
err = fmt.Errorf("the deployment needs a repair")
}
return actions, err
}
////////////////////////////////////////////////////////////////////////////////
// 'deployment-repair' subcommand.
func cmdRepairDeployment(params Parameters) *subcommands.Command {
return &subcommands.Command{
Advanced: true,
UsageLine: "deployment-repair [options]",
ShortDesc: "attempts to repair a deployment if it is broken",
LongDesc: "This is equivalent of running 'ensure' in paranoia.",
CommandRun: func() subcommands.CommandRun {
c := &repairDeploymentRun{}
c.registerBaseFlags()
c.clientOptions.registerFlags(&c.Flags, params, withRootDir, withMaxThreads)
return c
},
}
}
type repairDeploymentRun struct {
cipdSubcommand
clientOptions
}
func (c *repairDeploymentRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !c.checkArgs(args, 0, 0) {
return 1
}
ctx := cli.GetContext(a, c, env)
return c.done(repairDeployment(ctx, c.clientOptions))
}
func repairDeployment(ctx context.Context, clientOpts clientOptions) (cipd.ActionMap, error) {
client, err := clientOpts.makeCIPDClient(ctx)
if err != nil {
return nil, err
}
defer client.Close(ctx)
currentDeployment, err := client.FindDeployed(ctx)
if err != nil {
return nil, err
}
return client.EnsurePackages(ctx, currentDeployment, &cipd.EnsureOptions{
Paranoia: cipd.CheckIntegrity,
})
}
////////////////////////////////////////////////////////////////////////////////
// Main.
// GetApplication returns cli.Application.
//
// It can be used directly by subcommands.Run(...), or nested into another
// application.
func GetApplication(params Parameters) *cli.Application {
return &cli.Application{
Name: "cipd",
Title: "Chrome Infra Package Deployer (" + cipd.UserAgent + ")",
Context: func(ctx context.Context) context.Context {
loggerConfig := gologger.LoggerConfig{
Format: `[P%{pid} %{time:15:04:05.000} %{shortfile} %{level:.1s}] %{message}`,
Out: os.Stderr,
}
ctx, cancel := context.WithCancel(loggerConfig.Use(ctx))
signals.HandleInterrupt(cancel)
return ctx
},
EnvVars: map[string]subcommands.EnvVarDefinition{
cipd.EnvHTTPUserAgentPrefix: {
Advanced: true,
ShortDesc: "Optional http User-Agent prefix.",
},
cipd.EnvCacheDir: {
ShortDesc: "Directory with shared instance and tags cache " +
"(-cache-dir, if given, takes precedence).",
},
cipd.EnvMaxThreads: {
Advanced: true,
ShortDesc: "Number of worker threads for extracting packages. " +
"If 0 or negative, uses CPU count. (-max-threads, if given and not 0, takes precedence.)",
},
cipd.EnvParallelDownloads: {
Advanced: true,
ShortDesc: fmt.Sprintf("How many packages are allowed to be fetched concurrently. "+
"If <=1, packages will be fetched sequentially. Default is %d.", cipd.DefaultParallelDownloads),
},
cipd.EnvAdmissionPlugin: {
Advanced: true,
ShortDesc: "JSON-encoded list with a command line of a deployment admission plugin.",
},
cipd.EnvCIPDServiceURL: {
Advanced: true,
ShortDesc: "Override CIPD service URL.",
},
envSimpleTerminalUI: {
Advanced: true,
ShortDesc: "If set disables the fancy terminal UI with progress bars in favor of a simpler one that just logs to stderr.",
},
},
Commands: []*subcommands.Command{
subcommands.CmdHelp,
versioncli.CmdVersion(cipd.UserAgent),
// Authentication related commands.
{}, // These are spacers so that the commands appear in groups.
authcli.SubcommandInfo(params.DefaultAuthOptions, "auth-info", true),
authcli.SubcommandLogin(params.DefaultAuthOptions, "auth-login", false),
authcli.SubcommandLogout(params.DefaultAuthOptions, "auth-logout", false),
// High level read commands.
{},
cmdListPackages(params),
cmdSearch(params),
cmdResolve(params),
cmdDescribe(params),
cmdInstances(params),
// High level remote write commands.
{},
cmdCreate(params),
cmdAttach(params),
cmdSetRef(params),
cmdSetTag(params),
cmdSetMetadata(params),
// High level local write commands.
{},
cmdEnsure(params),
cmdSelfUpdate(params),
cmdSelfUpdateRoll(params),
// Advanced ensure file operations.
{Advanced: true},
cmdEnsureFileVerify(params),
cmdEnsureFileResolve(params),
// User friendly subcommands that operates within a site root. Implemented
// in friendly.go. These are advanced because they're half-baked.
{Advanced: true},
cmdInit(params),
cmdInstall(params),
cmdInstalled(params),
// ACLs.
{Advanced: true},
cmdListACL(params),
cmdEditACL(params),
cmdCheckACL(params),
// Low level pkg-* commands.
{Advanced: true},
cmdBuild(),
cmdDeploy(),
cmdFetch(params),
cmdInspect(),
cmdRegister(params),
// Low level deployment-* commands.
{Advanced: true},
cmdCheckDeployment(params),
cmdRepairDeployment(params),
// Low level misc commands.
{Advanced: true},
cmdPuppetCheckUpdates(params),
},
}
}
// Main runs the CIPD CLI.
//
func Main(params Parameters, args []string) int {
return subcommands.Run(GetApplication(params), fixflagpos.FixSubcommands(args))
}