blob: fa4e53a1ce707f7c60d6c365b56881c61d34115e [file]
// 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 gcloud contains helpers for calling `gcloud` tool in PATH.
package gcloud
import (
"context"
"encoding/json"
"os"
"os/exec"
"time"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/system/environ"
)
// Modules is a map "module name -> its versions".
type Modules map[string]Versions
// Versions is a map from version name to its attributes.
type Versions map[string]Version
// Version are attributes a version of some module.
type Version struct {
Name string // version name (matches the key in Versions map)
LastDeployedTime time.Time // when it was deployed
TrafficSplit float64 // 0.0 - 1.0
}
// List lists deployed versions of an app.
//
// Wraps `gcloud app versions list` command. If `module` is given, limits the
// output only to that module, otherwise lists all modules.
func List(ctx context.Context, appID, module string) (Modules, error) {
cmdLine := []string{
"gcloud", "app", "versions", "list",
"--format", "json",
"--project", appID,
}
if module != "" {
cmdLine = append(cmdLine, []string{"--service", module}...)
}
logging.Infof(ctx, "Running: %v", cmdLine)
cmd := exec.CommandContext(ctx, cmdLine[0], cmdLine[1:]...)
cmd.Stderr = os.Stderr
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, errors.Annotate(err, "failed to open stdout pipe").Err()
}
if err := cmd.Start(); err != nil {
return nil, errors.Annotate(err, "gcloud call failed to start").Err()
}
defer cmd.Wait() // release resources no matter what
// Note: this is a subset of fields we care about.
var listing []struct {
ID string `json:"id"` // version name really
Service string `json:"service"` // module name
TrafficSplit float64 `json:"traffic_split"` // 0.0 - 1.0
LastDeployedTime struct {
Datetime string `json:"datetime"` // "2020-03-20 17:42:19-07:00"
} `json:"last_deployed_time"`
}
if err := json.NewDecoder(stdout).Decode(&listing); err != nil {
return nil, errors.Annotate(err, "bad JSON in gcloud output").Err()
}
if err := cmd.Wait(); err != nil {
return nil, errors.Annotate(err, "gcloud call failed").Err()
}
// Convert to our preferred format.
out := Modules{}
for _, e := range listing {
vers := out[e.Service]
if vers == nil {
vers = Versions{}
out[e.Service] = vers
}
depTime, err := time.Parse("2006-01-02 15:04:05-07:00", e.LastDeployedTime.Datetime)
if err != nil {
return nil, errors.Annotate(err, "failed to parse deployment time %q of %s/%s",
e.LastDeployedTime.Datetime, e.Service, e.ID).Err()
}
vers[e.ID] = Version{
Name: e.ID,
TrafficSplit: e.TrafficSplit,
LastDeployedTime: depTime,
}
}
return out, nil
}
// Run executes arbitrary `gcloud [cmd]`.
func Run(ctx context.Context, cmd []string, cwd string, env environ.Env, dryRun bool) error {
cmdLine := append([]string{"gcloud"}, cmd...)
logging.Infof(ctx, "Running: %v", cmdLine)
if cwd != "" {
logging.Infof(ctx, " in cwd %q", cwd)
}
if dryRun {
logging.Warningf(ctx, "In dry run mode! Not really running anything.")
return nil
}
cmdObj := exec.CommandContext(ctx, cmdLine[0], cmdLine[1:]...)
cmdObj.Dir = cwd
cmdObj.Stdout = os.Stdout
cmdObj.Stderr = os.Stderr
if env != nil {
cmdObj.Env = env.Sorted()
}
if err := cmdObj.Run(); err != nil {
return errors.Annotate(err, "gcloud call failed").Err()
}
return nil
}