blob: 68006185769f907eb13667f0ce8d6e6e281e2cd0 [file] [log] [blame]
// goldpushk pushes Gold services to production. See go/goldpushk.
//
// Sample usage:
//
// Deployment of a specific service:
// $ goldpushk --service diffserver --instance chrome-gpu
// $ goldpushk -s diffserver -i chrome-gpu
//
// Deployment of a specific service across multiple instances:
// $ goldpushk --service diffserver --instance chrome-gpu,skia
// $ goldpushk -s diffserver -i chrome-gpu,skia
//
// Deployment of all instances of a given service across all Gold instances:
// $ goldpushk --service diffserver --instance all
// $ goldpushk -s diffserver -i all
//
// Deployment of all services corresponding to a specific Gold instance:
// $ goldpushk --service all --instance chrome-gpu
// $ goldpushk -s all -i chrome-gpu
//
// Deployment of all instances of a given service, designating one of them as the canary:
// $ goldpushk --service diffserver --instance all --canary skia:diffserver
//
// Print out all Gold instances and services goldpushk is able to manage:
// $ goldpushk --list
package main
import (
"context"
"errors"
"fmt"
"os"
"sort"
"strings"
"text/tabwriter"
"github.com/spf13/cobra"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/sklog/glog_and_cloud"
"go.skia.org/infra/go/util"
"go.skia.org/infra/golden/cmd/goldpushk/goldpushk"
)
const (
// Wildcard value for command line arguments.
all = "all"
// Environment variable with path to buildbot repository checkout directory.
skiaInfraRootEnvVar = "SKIA_INFRA_ROOT"
// Git repository with k8s configuration files in YAML format.
k8sConfigRepoUrl = "https://skia.googlesource.com/k8s-config"
)
var (
// Required flags.
flagInstances []string
flagServices []string
flagCanaries []string
// Optional flags.
flagList bool
flagDryRun bool
flagNoCommit bool
flagMinUptimeSeconds int
flagUptimePollFrequencySeconds int
// Flags for debugging.
flagLogToStdErr bool
flagVerbose bool
flagTesting bool
)
func main() {
// Prevent sklog from using glog.
glog_and_cloud.SetLogger(glog_and_cloud.NewStdErrCloudLogger(glog_and_cloud.SLogNone))
rootCmd := &cobra.Command{
Use: "goldpushk",
Long: "goldpushk pushes Gold services to production.",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if flagLogToStdErr {
glog_and_cloud.SetLogger(glog_and_cloud.NewStdErrCloudLogger(glog_and_cloud.SLogStderr))
}
},
Run: func(cmd *cobra.Command, args []string) {
run(cmd)
},
}
rootCmd.Flags().SortFlags = false
rootCmd.Flags().BoolVar(&flagList, "list", false, "List known Gold instances and services (tip: try combining this flag with --testing).")
rootCmd.Flags().StringSliceVarP(&flagInstances, "instances", "i", []string{}, "[REQUIRED] Comma-delimited list of Gold instances to target (e.g. \"skia,flutter\"), or \""+all+"\" to target all instances.")
rootCmd.Flags().StringSliceVarP(&flagServices, "services", "s", []string{}, "[REQUIRED] Comma-delimited list of services to target (e.g. \"frontend,diffserver\"), or \""+all+"\" to target all services.")
rootCmd.Flags().StringSliceVarP(&flagCanaries, "canaries", "c", []string{}, "Comma-delimited subset of Gold services to use as canaries, written as instance:service pairs (e.g. \"skia:diffserver,flutter:frontend\")")
rootCmd.Flags().BoolVar(&flagDryRun, "dryrun", false, "Do everything except applying the new configuration to Kubernetes and committing changes to Git.")
rootCmd.Flags().BoolVar(&flagNoCommit, "no-commit", false, "Do not commit configuration changes to the k8s-config repository.")
rootCmd.Flags().IntVar(&flagMinUptimeSeconds, "min-uptime", 30, "Minimum uptime in seconds required for all services before exiting the monitoring step.")
rootCmd.Flags().IntVar(&flagUptimePollFrequencySeconds, "poll-freq", 3, "How often to poll Kubernetes for service uptimes, in seconds.")
rootCmd.Flags().BoolVar(&flagLogToStdErr, "logtostderr", false, "Log debug information to stderr. No logs will be produced if this flag is not set.")
rootCmd.Flags().BoolVar(&flagVerbose, "verbose", false, "Verbose logs. This will log the commands executed and their command-line parameters.")
rootCmd.Flags().BoolVar(&flagTesting, "testing", false, "Do not deploy any production services; use testing services instead.")
// Fail with exit code 1 in the presence of invalid flags.
if _, err := rootCmd.ExecuteC(); err != nil {
sklog.Errorf("Failed to execute Cobra command: %s", err)
os.Exit(1)
}
}
func run(cmd *cobra.Command) {
// Get set of deployable units. Used as the source of truth across goldpushk.
var deployableUnitSet goldpushk.DeployableUnitSet
if flagTesting {
deployableUnitSet = goldpushk.TestingDeployableUnits()
} else {
deployableUnitSet = goldpushk.ProductionDeployableUnits()
}
// If --list is passed, print known services and exit. This takes into account flag --testing.
if flagList {
if err := listKnownServices(deployableUnitSet); err != nil {
sklog.Fatalf("Error while printing list of known services: %s", err)
}
return
}
// If --list was not provided, validate presence of flags --services and --instances.
if len(flagInstances) == 0 {
fmt.Println("Error: flag \"instances\" is required.")
if err := cmd.Usage(); err != nil {
sklog.Fatalf("Error while printing usage: %s", err)
}
os.Exit(1)
}
if len(flagServices) == 0 {
fmt.Println("Error: flag \"services\" is required.")
if err := cmd.Usage(); err != nil {
sklog.Fatalf("Error while printing usage: %s", err)
}
os.Exit(1)
}
// Parse and validate command line flags.
deployableUnits, canariedDeployableUnits, err := parseAndValidateFlags(deployableUnitSet, flagInstances, flagServices, flagCanaries)
if err != nil {
fmt.Printf("Error: %s.\n", err)
os.Exit(1)
}
// Read environment variables.
skiaInfraRoot, ok := os.LookupEnv(skiaInfraRootEnvVar)
if !ok {
fmt.Printf("Error: environment variable %s not set.\n", skiaInfraRootEnvVar)
os.Exit(1)
}
// Build goldpushk instance.
gpk := goldpushk.New(deployableUnits, canariedDeployableUnits, skiaInfraRoot, flagDryRun, flagNoCommit, flagMinUptimeSeconds, flagUptimePollFrequencySeconds, k8sConfigRepoUrl, flagVerbose)
// Run goldpushk.
if err = gpk.Run(context.Background()); err != nil {
fmt.Printf("Error: %s.\n", err)
os.Exit(1)
}
}
// listKnownServices prints out a table of known services.
func listKnownServices(deployableUnitSet goldpushk.DeployableUnitSet) error {
mode := "production"
if flagTesting {
mode = "testing"
}
fmt.Printf("Known Gold instances and services (%s):\n", mode)
// Print out table header.
w := tabwriter.NewWriter(os.Stdout, 10, 0, 2, ' ', 0)
if _, err := fmt.Fprintln(w, "\nINSTANCE\tSERVICE\tCANONICAL NAME"); err != nil {
return skerr.Wrap(err)
}
// Print out table body.
for _, instance := range deployableUnitSet.KnownInstances() {
for _, service := range deployableUnitSet.KnownServices() {
unit, ok := deployableUnitSet.Get(goldpushk.DeployableUnitID{Instance: instance, Service: service})
if ok {
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\n", instance, service, unit.CanonicalName()); err != nil {
return skerr.Wrap(err)
}
}
}
}
// Flush output and return.
if err := w.Flush(); err != nil {
return skerr.Wrap(err)
}
return nil
}
// containsWildcardValue determines whether or not a flag contains the special
// "all" wildcard value.
func containsWildcardValue(flag []string) bool {
for _, value := range flag {
if value == all {
return true
}
}
return false
}
// parseAndValidateFlags validates the given command line flags, retrieves the
// corresponding DeployableUnits from the given DeployableUnitSet and returns
// them as two separate slices according to whether or not they were marked for
// canarying.
func parseAndValidateFlags(deployableUnitSet goldpushk.DeployableUnitSet, instances, services, canaries []string) (deployableUnits, canariedDeployableUnits []goldpushk.DeployableUnit, err error) {
// Deduplicate inputs.
instances = util.SSliceDedup(instances)
services = util.SSliceDedup(services)
canaries = util.SSliceDedup(canaries)
// Determine whether --instances or --services are set to "all".
allInstances := containsWildcardValue(instances)
allServices := containsWildcardValue(services)
// If --instances or --services contain the special "all" value, they should
// not contain any other values.
if allInstances && len(instances) != 1 {
return nil, nil, errors.New("flag --instances should contain either \"all\" or a list of Gold instances, but not both")
}
if allServices && len(services) != 1 {
return nil, nil, errors.New("flag --services should contain either \"all\" or a list of Gold services, but not both")
}
if allInstances && allServices {
return nil, nil, errors.New("cannot set both --instances and --services to \"all\"")
}
// Validate instances.
if !allInstances {
for _, instanceStr := range instances {
if !deployableUnitSet.IsKnownInstance(goldpushk.Instance(instanceStr)) {
return nil, nil, fmt.Errorf("unknown Gold instance: \"%s\"", instanceStr)
}
}
}
// Validate services.
if !allServices {
for _, serviceStr := range services {
if !deployableUnitSet.IsKnownService(goldpushk.Service(serviceStr)) {
return nil, nil, fmt.Errorf("unknown Gold service: \"%s\"", serviceStr)
}
}
}
// This slice will be populated with the subset of the cartesian product of
// flags --instances and --services that is found in the services map.
var servicesToDeploy []goldpushk.DeployableUnitID
// Determines whether or not an instance/service pair should be canaried.
isMarkedForCanarying := map[goldpushk.DeployableUnitID]bool{}
// Determine the set of instances over which to iterate to compute the
// cartesian product of flags --instances and --services.
var instanceIterationSet []goldpushk.Instance
if containsWildcardValue(instances) {
// Handle the "all" value.
instanceIterationSet = deployableUnitSet.KnownInstances()
} else {
for _, instanceStr := range instances {
instanceIterationSet = append(instanceIterationSet, goldpushk.Instance(instanceStr))
}
}
// Determine the set of services over which to iterate to compute the
// cartesian product of flags --instances and --services.
var serviceIterationSet []goldpushk.Service
if containsWildcardValue(services) {
// Handle the "all" value.
serviceIterationSet = deployableUnitSet.KnownServices()
} else {
for _, serviceStr := range services {
serviceIterationSet = append(serviceIterationSet, goldpushk.Service(serviceStr))
}
}
// Iterate over the cartesian product of flags --instances and --services.
for _, instance := range instanceIterationSet {
for _, service := range serviceIterationSet {
id := goldpushk.DeployableUnitID{
Instance: instance,
Service: service,
}
// Skip if the current instance/service combination is not found in the services map.
if _, ok := deployableUnitSet.Get(id); !ok {
continue
}
// Save instance/service pair, which is not marked for canarying by default.
servicesToDeploy = append(servicesToDeploy, id)
isMarkedForCanarying[id] = false
}
}
// Fail if --instances and --services didn't match any services in the services map.
if len(servicesToDeploy) == 0 {
return nil, nil, errors.New("no known Gold services match the values supplied with --instances and --services")
}
// Iterate over the --canaries flag.
for _, canaryStr := range canaries {
// Validate format and extract substrings.
canaryStrSplit := strings.Split(canaryStr, ":")
if len(canaryStrSplit) != 2 || len(canaryStrSplit[0]) == 0 || len(canaryStrSplit[1]) == 0 {
return nil, nil, fmt.Errorf("invalid canary format: \"%s\"", canaryStr)
}
instance := goldpushk.Instance(canaryStrSplit[0])
service := goldpushk.Service(canaryStrSplit[1])
instanceServicePair := goldpushk.DeployableUnitID{
Instance: instance,
Service: service,
}
// Validate canary subcomponents.
if !deployableUnitSet.IsKnownInstance(instance) {
return nil, nil, fmt.Errorf("invalid canary - unknown Gold instance: \"%s\"", canaryStr)
}
if !deployableUnitSet.IsKnownService(service) {
return nil, nil, fmt.Errorf("invalid canary - unknown Gold service: \"%s\"", canaryStr)
}
// Canaries should match the services provided with --instances and --services.
if _, ok := isMarkedForCanarying[instanceServicePair]; !ok {
return nil, nil, fmt.Errorf("canary does not match any targeted services: \"%s\"", canaryStr)
}
// Mark instance/service pair for canarying.
isMarkedForCanarying[instanceServicePair] = true
}
// Sort services to deploy to generate a deterministic output.
sort.Slice(servicesToDeploy, func(i, j int) bool {
a := servicesToDeploy[i]
b := servicesToDeploy[j]
return a.Instance < b.Instance || (a.Instance == b.Instance && a.Service < b.Service)
})
// Build outputs.
for _, instanceServicePair := range servicesToDeploy {
deployment, ok := deployableUnitSet.Get(instanceServicePair)
if !ok {
sklog.Fatalf("DeployableUnit \"%s\" not found in deployableUnitSet", deployment.CanonicalName())
}
if isMarkedForCanarying[instanceServicePair] {
canariedDeployableUnits = append(canariedDeployableUnits, deployment)
} else {
deployableUnits = append(deployableUnits, deployment)
}
}
// If all services to be deployed are marked for canarying, it probably
// indicates a user error.
if len(deployableUnits) == 0 {
return nil, nil, errors.New("all targeted services are marked for canarying")
}
return deployableUnits, canariedDeployableUnits, nil
}