fast-build-update-tool/internal/config/cli_args.go (109 lines of code) (raw):
// config holds any logic around application configuration, logging, common constants, etc...
package config
import (
"errors"
"flag"
"fmt"
"net"
"os"
"strings"
)
// CLIArgs holds the parsed and validated args the user passed to the application
type CLIArgs struct {
// FleetId is the id of the fleet the user would like to update
FleetId string
// IpRange is the range of IP address that are allowed remote access to the GameLift fleet
IpRange string
// BuildZipPath is the path on the local filesystem to the build zip file
BuildZipPath string
// PrivateKeyPath is the path on the local filesystem to the private SSH key that will be used to interact with remote instances
PrivateKeyPath string
// SSHPort is the port that will be opened for SSH use on any remote instances
SSHPort int
// InstanceIds is an optional allow list of instance ids to update in GameLift
InstanceIds []string
// RestartProcess optional flag to skip uploading and replacing a build, and simply restart the server process on remote instances
RestartProcess bool
// LockName is an optional override to change the name of the lock file used on remote servers in-case of deadlock.
LockName string
// Verbose is an optional argument to provide more verbose application logs
Verbose bool
instanceIdsRaw string
}
// ParseAndValidateCLIArgs will parse the input slice of string arguments, and validate them
func ParseAndValidateCLIArgs(cliArgs []string) (CLIArgs, error) {
result, err := ParseArgs(cliArgs)
if err != nil {
return result, err
}
return result, result.Validate()
}
const (
argFleetId = "fleet-id"
argIpRange = "ip-range"
argBuildZipPath = "zip-path"
argPrivateKey = "private-key"
argSSHPort = "ssh-port"
argInstanceIds = "instance-ids"
argRestartProcess = "restart-process"
argLockName = "lock-name"
argVerbose = "verbose"
)
// ParseArgs will parse the input slice of string arguments into CLIArgs
func ParseArgs(args []string) (CLIArgs, error) {
result := CLIArgs{}
flags := flag.NewFlagSet(AppName, flag.ContinueOnError)
// Define required arguments
flags.StringVar(&result.FleetId, argFleetId, "", "[Required] The ID of the GameLift Fleet to update")
flags.StringVar(&result.IpRange, argIpRange, "", "[Required] Your local IP Address, needed to open ports on the fleet for remote connections (eg. 127.0.0.1/32)")
flags.StringVar(&result.BuildZipPath, argBuildZipPath, "", "[Required] The path to the zip file containing your build")
flags.StringVar(&result.PrivateKeyPath, argPrivateKey, "", "[Required] The local path to a private key to be used with SSH")
// Define optional arguments
flags.IntVar(&result.SSHPort, argSSHPort, 0, "[Optional] The port to open for SSH on the fleet. This option is for Windows remote instances only. It will default to 1026.")
flags.StringVar(&result.instanceIdsRaw, argInstanceIds, "", "[Optional] A list of instance ids to update separated by comma. If not provided all instances will be updated")
flags.BoolVar(&result.RestartProcess, argRestartProcess, false, "[Optional] Flag to restart existing game server processes on a server, and skip uploading a new build and replacing the old build.")
flags.StringVar(&result.LockName, argLockName, AppName, "[Optional] This should only be set if you encounter a deadlock. This should not be set in typical application use. Set this argument to manually override the lock file name used on the server if your application gets stuck in an update deadlock.")
flags.BoolVar(&result.Verbose, argVerbose, false, "[Optional] Write more verbose logs as output")
flags.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s --%s FLEET_ID --%s IP_RANGE --%s BUILD_ZIP_PATH --%s PRIVATE_KEY \n", os.Args[0], argFleetId, argIpRange, argBuildZipPath, argPrivateKey)
flags.PrintDefaults()
}
// If nothing was passed at all, show the usage instructions
if len(args) <= 1 {
flags.Usage()
return result, flag.ErrHelp
}
// Parse the arguments (without the application exe in the slice)
err := flags.Parse(args[1:])
if err != nil {
return result, err
}
// Split instance id CSV into a slice if provided
if result.instanceIdsRaw != "" {
result.InstanceIds = strings.Split(result.instanceIdsRaw, ",")
}
return result, nil
}
// Validate that all of the CLIArgs are valid
func (c *CLIArgs) Validate() (err error) {
if c.FleetId == "" {
err = errors.Join(err, missingArgumentError(argFleetId))
}
if c.IpRange == "" {
err = errors.Join(err, missingArgumentError(argIpRange))
} else if !isValidIpRange(c.IpRange) {
err = errors.Join(err, invalidArgumentError(argIpRange, "must be a valid IP range"))
}
// We do not need to validate a build zip file if we are just restarting the process
if c.RestartProcess {
if c.BuildZipPath != "" {
err = errors.Join(err, invalidArgumentError(argBuildZipPath, "zip file provided along with restart process flag"))
}
} else {
if c.BuildZipPath == "" {
err = errors.Join(err, missingArgumentError(argBuildZipPath))
} else if !doesFileExist(c.BuildZipPath) {
err = errors.Join(err, missingFileError(argBuildZipPath))
}
}
if c.PrivateKeyPath == "" {
err = errors.Join(err, missingArgumentError(argPrivateKey))
} else if !doesFileExist(c.PrivateKeyPath) {
err = errors.Join(err, missingFileError(argPrivateKey))
}
return err
}
// GetUpdateOperation will return what update operation the CLIArgs have instructed the app to take
func (c *CLIArgs) GetUpdateOperation() UpdateOperation {
// Currently only two options here (restart processes, or replace the build on all instances)
if c.RestartProcess {
return UpdateOperationRestartProcess
}
return UpdateOperationReplaceBuild
}
func doesFileExist(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func isValidIpRange(ipRange string) bool {
_, _, cidrErr := net.ParseCIDR(ipRange)
return cidrErr == nil
}