blob: 8043c6e98b314560341a08b97937a65539ac4dbe [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/maruel/subcommands"
"go.chromium.org/luci/common/cli"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/system/signals"
"infra/cmd/gaedeploy/cache"
"infra/cmd/gaedeploy/source"
)
// execCb a signature of a function that executes a subcommand.
type execCb func(ctx context.Context) error
// Placeholders for some CLI flags that indicate they weren't set.
const (
appIDPlaceholder = "<app-id>"
tarballSourcePlaceholder = "<path>"
)
// commandBase defines flags common to all subcommands.
type commandBase struct {
subcommands.CommandRunBase
extraFlags extraFlags // as passed to init(...)
exec execCb // called to actually execute the command
logConfig logging.Config // -log-* flags
appID string // -app-id flag (required)
tarballSource string // -tarball-source flag (required)
tarballSHA256 string // -tarball-sha256 flag (optional for local files)
cacheDir string // -cache-dir flag (optional, has default)
dryRun bool // -dry-run flag
source source.Source // initialized in handleArgsAndFlags
cache *cache.Cache // initialized in handleArgsAndFlags
}
// extraFlags indicates what CLI flags to register.
type extraFlags struct {
appID bool // -app-id
tarball bool // -tarball-*
cacheDir bool // -cache-dir
dryRun bool // -dry-run
}
// init register base flags. Must be called.
func (c *commandBase) init(exec execCb, extraFlags extraFlags) {
c.extraFlags = extraFlags
c.exec = exec
c.logConfig.Level = logging.Info // default logging level
c.logConfig.AddFlags(&c.Flags)
if extraFlags.appID {
c.Flags.StringVar(&c.appID, "app-id", appIDPlaceholder, "GAE app ID to update.")
}
if extraFlags.tarball {
c.Flags.StringVar(&c.tarballSource, "tarball-source", tarballSourcePlaceholder, "Either gs:// or local path to a tarball with app source code.")
c.Flags.StringVar(&c.tarballSHA256, "tarball-sha256", "", "The expected tarball's SHA256 (optional for local files).")
}
if extraFlags.cacheDir {
c.Flags.StringVar(&c.cacheDir, "cache-dir", "", "Directory to keep unpacked tarballs in.")
}
if extraFlags.dryRun {
c.Flags.BoolVar(&c.dryRun, "dry-run", false, "Just print gcloud commands that modify state without executing them.")
}
}
// ModifyContext implements cli.ContextModificator.
//
// Used by cli.Application.
func (c *commandBase) ModifyContext(ctx context.Context) context.Context {
return c.logConfig.Set(ctx)
}
// Run implements the subcommands.CommandRun interface.
func (c *commandBase) Run(a subcommands.Application, args []string, env subcommands.Env) int {
ctx := cli.GetContext(a, c, env)
logging.Infof(ctx, "Starting %s", UserAgent)
if err := c.handleArgsAndFlags(args, env); err != nil {
return handleErr(ctx, isCLIError.Apply(err))
}
ctx, cancel := context.WithCancel(ctx)
signals.HandleInterrupt(cancel)
if err := c.exec(ctx); err != nil {
return handleErr(ctx, err)
}
return 0
}
// handleArgsAndFlags validates flags and substitutes defaults.
func (c *commandBase) handleArgsAndFlags(args []string, env subcommands.Env) error {
switch {
case len(args) != 0:
return errors.Reason("unexpected positional arguments %q", args).Err()
case c.extraFlags.appID && c.appID == appIDPlaceholder:
return errBadFlag("-app-id", "a value is required")
case c.extraFlags.tarball && c.tarballSource == tarballSourcePlaceholder:
return errBadFlag("-tarball-source", "a value is required")
}
// Where to grab the tarball from.
if c.extraFlags.tarball {
var err error
c.source, err = source.New(c.tarballSource, c.tarballSHA256)
if err != nil {
return err
}
}
// Where to store it.
if c.extraFlags.cacheDir {
cacheDir := ""
switch {
case c.cacheDir != "":
cacheDir = c.cacheDir
case env["LUCI_GAEDEPLOY_CACHE_DIR"].Value != "":
cacheDir = env["LUCI_GAEDEPLOY_CACHE_DIR"].Value
default:
home, err := os.UserHomeDir()
if err != nil {
return errors.Annotate(err, "failed to determine the home dir, pass -cache-dir directly").Err()
}
cacheDir = filepath.Join(home, ".gaedeploy_cache")
}
if err := os.MkdirAll(cacheDir, 0700); err != nil {
return errors.Annotate(err, "failed to create the cache directory").Err()
}
c.cache = &cache.Cache{Root: cacheDir}
}
return nil
}
// isCLIError is tagged into errors caused by bad CLI flags.
var isCLIError = errors.BoolTag{Key: errors.NewTagKey("bad CLI invocation")}
// errBadFlag produces an error related to malformed or absent CLI flag
func errBadFlag(flag, msg string) error {
return errors.Reason("bad %q: %s", flag, msg).Tag(isCLIError).Err()
}
// handleErr prints the error and returns the process exit code.
func handleErr(ctx context.Context, err error) int {
switch {
case err == nil:
return 0
case errors.Contains(err, context.Canceled): // happens on Ctrl+C
fmt.Fprintf(os.Stderr, "%s\n", err)
return 3
case isCLIError.In(err):
fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err)
return 2
default:
logging.Errorf(ctx, "%s", err)
logging.Errorf(ctx, "Full context:")
errors.Log(ctx, err)
return 1
}
}