tools/mig-scaler/main.go (113 lines of code) (raw):

/* Copyright 2022 Google LLC 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 https://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 main import ( "context" "errors" "fmt" "log" "os" "time" "github.com/spf13/pflag" execpb "google.golang.org/genproto/googleapis/cloud/workflows/executions/v1" ) type Job struct { ID string StartTime time.Time EndTime time.Time State execpb.Execution_State MIG MIGRef TargetSize uint32 Increment uint32 Wait time.Duration Deadline time.Time } type JobPayload struct { // MaxUint32 is less than javascript's MAX_SAFE_INTEGER so there's no need // to use a string for the json representation of uint32 Project string `json:"project"` Region string `json:"region"` Zone string `json:"zone"` Name string `json:"name"` TargetSize uint32 `json:"target_size"` Increment uint32 `json:"increment"` Wait uint32 `json:"wait"` Deadline time.Time `json:"deadline"` } type Command interface { Execute(ctx context.Context) error } func main() { var err error log.SetFlags(0) cfg := new(Config) f := pflag.NewFlagSet("mig-scaler", pflag.ContinueOnError) // setup flags before reading the config files , otherwise the pflag package // will overwrite the config with the default values f.StringVar(&cfg.Workflow.Project, "workflow-project", "", "") f.StringVar(&cfg.Workflow.Region, "workflow-region", "", "") f.StringVar(&cfg.Workflow.Name, "workflow-name", "mig-scaler", "") f.Uint32Var(&cfg.Workflow.MaxSize, "workflow-max-size", 100, "") f.DurationVar(&cfg.Workflow.MaxDuration, "workflow-max-duration", 8*time.Hour, "") f.StringVar(&cfg.MIG.Project, "project", "", "") f.StringVar(&cfg.MIG.Region, "region", "", "") f.StringVar(&cfg.MIG.Zone, "zone", "", "") // default rate of 10 machines per minute // if the clients have 32 cores this will start 9,600 cores in 30 minutes. f.Uint32Var(&cfg.MIG.Increment, "increment", 10, "") f.DurationVar(&cfg.MIG.Wait, "wait", 1*time.Minute, "") f.DurationVar(&cfg.MIG.Duration, "duration", 0, "") f.StringVar(&cfg.Format, "format", "table", "") f.BoolVar(&cfg.Detailed, "detailed", false, "") f.BoolVar(&cfg.ActiveOnly, "active", false, "") f.BoolVarP(&cfg.Help, "help", "h", false, "") // read the config file before parsing the command line arguments so // that the command line arguments override any config values err = readDefaultConfig(cfg) if err != nil { log.Printf("error: could not read config: %s", err) os.Exit(2) } // override values from the config file with environment variables readEnv(cfg) // command line arguments overrides all other sources err = f.Parse(os.Args[1:]) // grab the positional args before checking for an error so that they can // be used by ShowUsage cfg.Args = f.Args() if len(cfg.Args) >= 1 { cfg.Command = cfg.Args[0] cfg.Args = cfg.Args[1:] } if err != nil { log.Printf("error: %+v", err) showUsage(cfg) os.Exit(2) } if f.Lookup("region").Changed && !f.Lookup("zone").Changed { // if the region was set, and the zone was not set, clear the zone // so that setting --region=value on the command line overrides zone // from the environment or config cfg.MIG.Zone = "" } // Default the workflow location to the MIG location if the workflow was // not set. This allows running with just --project=... --region=... without // an ini file. defaultString(&cfg.Workflow.Project, cfg.MIG.Project) defaultString(&cfg.Workflow.Region, cfg.MIG.Region) // Likewise, default the job's max duration to the workflow's max duration // if the job's max duration was not set. defaultDuration(&cfg.MIG.Duration, cfg.Workflow.MaxDuration) // Truncate durations to the nearest second truncateDuration(&cfg.Workflow.MaxDuration) truncateDuration(&cfg.MIG.Wait) truncateDuration(&cfg.MIG.Duration) cmd, err := CreateCommand(cfg) if err != nil { printConfigError(err) showUsage(cfg) os.Exit(2) } ctx := context.Background() err = cmd.Execute(ctx) if err != nil { log.Fatalf("error: %+v", err) } } func CreateCommand(cfg *Config) (Command, error) { name := cfg.Command if cfg.Help { // If the help flag has been set, show the help instead, but do not // change the command so that the help can use it for the topic. name = "help" } switch name { case "": return nil, errors.New("command not specified") case "list": return NewList(cfg) case "scale": return NewScale(cfg) case "cancel": return NewCancel(cfg) case "help": return NewHelp(cfg), nil default: return nil, fmt.Errorf("unknown command: \"%s\"", name) } }