blob: 8545906fc21ade84a03bd99125aa2e8decf168be [file] [log] [blame]
// pushcli is a simple command-line application for pushing a package to head.
package main
import (
"flag"
"fmt"
"net/http"
"os/user"
"strings"
"go.skia.org/infra/go/auth"
"go.skia.org/infra/go/chatbot"
"go.skia.org/infra/go/common"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/packages"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/push/go/trigger"
"google.golang.org/api/compute/v1"
"google.golang.org/api/storage/v1"
)
const (
CHAT_MSG = `%s pushed %s to %s`
)
var (
project = flag.String("project", "google.com:skia-buildbots", "The Google Compute Engine project.")
rollback = flag.Bool("rollback", false, "If true roll back to the next most recent package, otherwise use the most recently pushed package.")
force = flag.Bool("force", false, "If true then install the package even if it hasn't previously been installed on the given server.")
dryrun = flag.Bool("dryrun", false, "If true don't actually push, but just log what actions would be taken.")
configFilename = flag.String("config_filename", "allskiapush.json5", "Config filename used by Push.")
)
func init() {
flag.Usage = func() {
fmt.Printf(`Usage: pushcli [options] <package> <server>
Pushes the latest version of <package> to <server>.
<package> - The name of the package, e.g. "pulld"
<server> - The name of the server, e.g. "skia-monitoring". Can be * to affect all servers that currently have the package installed.
Use the --rollback flag to force a rollback to the previous version. Note that this always picks
the next most recent package, regardless of the version of the package currently deployed.
`)
flag.PrintDefaults()
}
}
func main() {
common.Init()
// Parse out the non-flag arguments.
args := flag.Args()
if len(args) != 2 {
sklog.Errorf("Requires two arguments. Saw %q\n", args)
flag.Usage()
return
}
appName := args[0] // "skdebuggerd"
serverName := args[1] // "skia-debugger" or "*"
// Create the needed clients.
tokenSource := auth.NewGCloudTokenSource("")
client := httputils.DefaultClientConfig().WithTokenSource(tokenSource).With2xxOnly().Client()
store, err := storage.New(client)
if err != nil {
sklog.Fatalf("Failed to create storage service client: %s", err)
}
comp, err := compute.New(client)
if err != nil {
sklog.Fatalf("Failed to create compute service client: %s", err)
}
chatbot.Init("pushcli")
servers, err := expand(appName, serverName)
if err != nil {
sklog.Fatalf("Failed to enumerate servers: %s", err)
}
sklog.Infof("Installing %s to servers %q", appName, servers)
for _, s := range servers {
if err := installOnServer(client, store, comp, appName, s); err != nil {
sklog.Fatalf(err.Error())
}
}
}
// expand returns a slice of the server names that should be affected. If 's' is "*", it will look up all
// instances in the project and return the list of instance names that have appName installed.
func expand(appName, s string) ([]string, error) {
if s != "*" {
return []string{s}, nil
}
config, err := packages.LoadPackageConfig(*configFilename)
if err != nil {
return nil, fmt.Errorf("Failed to load PackageConfig file %s: %s", *configFilename, err)
}
return config.AllServerNamesWithPackage(appName), nil
}
// installOnServer installs the named app on the compute engine instance of the given name. It then tries to force
// pulld to pick up the changes by pinging the ip address of the server directly.
func installOnServer(client *http.Client, store *storage.Service, comp *compute.Service, appName, serverName string) error {
// Get the current set of packages installed on the server.
installed, err := packages.InstalledForServer(client, store, serverName)
if err != nil {
return fmt.Errorf("Failed to get the current installed packages on %s: %s", serverName, err)
}
sklog.Infof("Installed Packages on %s:\n%s", serverName, strings.Join(installed.Names, "\n"))
// Get the sorted list of available versions of the given package.
available, err := packages.AllAvailableApp(store, appName)
if err != nil {
return fmt.Errorf("Failed to get the list of available versions for package %s: %s", appName, err)
}
sklog.Infof("Available: %s", packages.PackageSlice(available).String())
// By default roll to head, which is the first entry in the slice.
latest := available[0]
if *rollback {
if len(available) == 1 {
return fmt.Errorf("Can't rollback a package with only one version.")
}
latest = available[1]
}
found := false
// Build a new list of packages that is the old list of packages with the new package added.
newInstalled := []string{fmt.Sprintf("%s/%s", appName, latest.Name)}
for _, name := range installed.Names {
if strings.Split(name, "/")[0] == appName {
found = true
continue
}
newInstalled = append(newInstalled, name)
}
if !found && !*force {
return fmt.Errorf("The application %s isn't currently installed on server %s. (Use --force to override.)", appName, serverName)
}
if *dryrun {
sklog.Info("Is in dry run mode. Would be calling")
sklog.Infof(`packages.PutInstalled(store, "%s", %q, %d)`, serverName, newInstalled, installed.Generation)
} else {
// Write the new list of packages back to Google Storage.
if err := packages.PutInstalled(store, serverName, newInstalled, installed.Generation); err != nil {
return fmt.Errorf("Failed to write updated package for %s: %s", appName, err)
}
}
// We are running locally, so we need to use the Compute API to read the
// values of project level metadata. This closure will be passed to
// chatbot.SendUsingConfig(configReader).
configReader := func() string {
prj, err := comp.Projects.Get(*project).Do()
if err != nil {
sklog.Warningf("Failed to retrieve metadata needed for chatbot: %s", err)
return ""
} else {
ret := ""
for _, item := range prj.CommonInstanceMetadata.Items {
if item.Key == chatbot.BOT_WEBHOOK_METADATA_KEY {
ret = *item.Value
}
}
return ret
}
}
username := ""
userinfo, err := user.Current()
if err == nil {
username = userinfo.Username
}
body := fmt.Sprintf(CHAT_MSG, username, appName, serverName)
if err := chatbot.SendUsingConfig(body, "push", "", configReader); err != nil {
sklog.Warningf("Failed to send chat notification: %s", err)
}
zone, err := findZone(comp, serverName)
if err != nil {
sklog.Warningf("Could not find zone: %s", err)
return nil
}
if err := trigger.ByMetadata(comp, *project, latest.Name, serverName, zone); err != nil {
sklog.Warningf("Could not trigger package load via metadata: %s", err)
}
return nil
}
// findZone returns the zone of the server with the given name.
func findZone(comp *compute.Service, name string) (string, error) {
// We have to look in each zone for the server with the given name.
zones, err := comp.Zones.List(*project).Do()
if err != nil {
return "", fmt.Errorf("Failed to list zones: %s", err)
}
for _, zone := range zones.Items {
_, err := comp.Instances.Get(*project, zone.Name, name).Do()
if err == nil {
return zone.Name, nil
}
}
return "", fmt.Errorf("Couldn't find an instance named: %s", name)
}