15
cmd/init.go
15
cmd/init.go
@@ -1,15 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"speedrun/config"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize speedrun",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return config.Create()
|
||||
},
|
||||
}
|
||||
181
cmd/key.go
181
cmd/key.go
@@ -1,181 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"speedrun/cloud"
|
||||
"speedrun/key"
|
||||
|
||||
"github.com/apex/log"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var keyCmd = &cobra.Command{
|
||||
Use: "key",
|
||||
Short: "Manage ssh keys",
|
||||
TraverseChildren: true,
|
||||
}
|
||||
|
||||
var newKeyCmd = &cobra.Command{
|
||||
Use: "new",
|
||||
Short: "Create a new ssh key",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
initConfig()
|
||||
},
|
||||
RunE: newKey,
|
||||
}
|
||||
|
||||
var authorizeKeyCmd = &cobra.Command{
|
||||
Use: "authorize",
|
||||
Short: "Authorize key for ssh access",
|
||||
Example: " speedrun key authorize",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
initConfig()
|
||||
},
|
||||
RunE: authorizeKey,
|
||||
}
|
||||
|
||||
var revokeKeyCmd = &cobra.Command{
|
||||
Use: "revoke",
|
||||
Short: "Revoke ssh key",
|
||||
Example: " speedrun key revoke",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
initConfig()
|
||||
},
|
||||
RunE: revokeKey,
|
||||
}
|
||||
|
||||
var listKeysCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List OS Login keys",
|
||||
Example: " speedrun key list",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
initConfig()
|
||||
},
|
||||
RunE: listKeys,
|
||||
}
|
||||
|
||||
func init() {
|
||||
keyCmd.AddCommand(newKeyCmd)
|
||||
keyCmd.AddCommand(authorizeKeyCmd)
|
||||
keyCmd.AddCommand(revokeKeyCmd)
|
||||
keyCmd.AddCommand(listKeysCmd)
|
||||
authorizeKeyCmd.Flags().Bool("use-oslogin", false, "Authorize the key via OS Login rather than metadata")
|
||||
viper.BindPFlag("gcp.use-oslogin", authorizeKeyCmd.Flags().Lookup("use-oslogin"))
|
||||
}
|
||||
|
||||
func determineKeyFilePath() (string, error) {
|
||||
log.Debug("Determining private key path")
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
path := filepath.Join(home, ".speedrun/privatekey")
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func newKey(cmd *cobra.Command, args []string) error {
|
||||
k, err := key.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path, err := determineKeyFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = k.Write(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("Private key created")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func authorizeKey(cmd *cobra.Command, args []string) error {
|
||||
project := viper.GetString("gcp.projectid")
|
||||
useOSlogin := viper.GetBool("gcp.use-oslogin")
|
||||
|
||||
gcpClient, err := cloud.NewGCPClient(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path, err := determineKeyFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k, err := key.Read(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if useOSlogin {
|
||||
gcpClient.AddUserKey(k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("Authorized key via OS Login")
|
||||
} else {
|
||||
gcpClient.AddKeyToMetadata(k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("Authorized key in the project metadata")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func revokeKey(cmd *cobra.Command, args []string) error {
|
||||
project := viper.GetString("gcp.projectid")
|
||||
gcpClient, err := cloud.NewGCPClient(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path, err := determineKeyFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k, err := key.Read(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("Revoking public key")
|
||||
err = gcpClient.RemoveKeyFromMetadata(k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = gcpClient.RemoveUserKey(k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listKeys(cmd *cobra.Command, args []string) error {
|
||||
project := viper.GetString("gcp.projectid")
|
||||
gcpClient, err := cloud.NewGCPClient(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("Fetching OS Login keys")
|
||||
err = gcpClient.ListUserKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
59
cmd/portal/cli/init.go
Normal file
59
cmd/portal/cli/init.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/pelletier/go-toml"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initCmd.Flags().BoolP("print", "p", false, "Print default config to stdout")
|
||||
}
|
||||
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize portal",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
print, err := cmd.Flags().GetBool("print")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if print {
|
||||
c := viper.AllSettings()
|
||||
bs, err := toml.Marshal(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal config: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(bs))
|
||||
return nil
|
||||
}
|
||||
|
||||
dir := filepath.Dir(cfgFile)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err = os.Mkdir(dir, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = viper.SafeWriteConfigAs(cfgFile)
|
||||
if err != nil {
|
||||
if _, ok := err.(viper.ConfigFileAlreadyExistsError); !ok {
|
||||
return fmt.Errorf("couldn't save config at \"%s\" (%s)", viper.ConfigFileUsed(), err)
|
||||
}
|
||||
} else {
|
||||
log.Infof("Your config was saved at \"%s\"", viper.ConfigFileUsed())
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("Config already exists at \"%s\", no changes applied", viper.ConfigFileUsed())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
128
cmd/portal/cli/root.go
Normal file
128
cmd/portal/cli/root.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/apex/log"
|
||||
jsonhandler "github.com/apex/log/handlers/json"
|
||||
texthandler "github.com/apex/log/handlers/text"
|
||||
"github.com/speedrunsh/speedrun/pkg/common/cryptoutil"
|
||||
"github.com/speedrunsh/speedrun/pkg/portal"
|
||||
portalpb "github.com/speedrunsh/speedrun/proto/portal"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"storj.io/drpc/drpcmux"
|
||||
"storj.io/drpc/drpcserver"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
var version string
|
||||
var commit string
|
||||
var date string
|
||||
|
||||
func Execute() {
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "portal",
|
||||
Short: "Control your compute fleet at scale",
|
||||
Version: fmt.Sprintf("%s, commit: %s, date: %s", version, commit, date),
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
insecure := viper.GetBool("tls.insecure")
|
||||
caPath := viper.GetString("tls.ca")
|
||||
certPath := viper.GetString("tls.cert")
|
||||
keyPath := viper.GetString("tls.key")
|
||||
|
||||
m := drpcmux.New()
|
||||
var err error
|
||||
err = portalpb.DRPCRegisterPortal(m, &portal.Server{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not register DRPC server: %v", err)
|
||||
}
|
||||
s := drpcserver.New(m)
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
if insecure {
|
||||
log.Warn("Using insecure TLS configuration, this should be avoided in production environments")
|
||||
tlsConfig, err = cryptoutil.InsecureTLSConfig()
|
||||
} else {
|
||||
tlsConfig, err = cryptoutil.ServerTLSConfig(caPath, certPath, keyPath)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not initialize TLS config: %v", err)
|
||||
}
|
||||
|
||||
port := viper.GetInt("port")
|
||||
ip := viper.GetString("address")
|
||||
addr := fmt.Sprintf("%s:%d", ip, port)
|
||||
lis, err := tls.Listen("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create TCP socket: %v", err)
|
||||
}
|
||||
defer lis.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
log.Infof("Starting portal on %s", addr)
|
||||
return s.Serve(ctx, lis)
|
||||
},
|
||||
}
|
||||
|
||||
dir := "/etc/portal"
|
||||
configPath := filepath.Join(dir, "config.toml")
|
||||
|
||||
cobra.OnInitialize(initConfig)
|
||||
rootCmd.AddCommand(initCmd)
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", configPath, "config file")
|
||||
rootCmd.PersistentFlags().StringP("loglevel", "l", "info", "Log level")
|
||||
rootCmd.PersistentFlags().BoolP("json", "j", false, "Output logs in JSON format")
|
||||
rootCmd.Flags().IntP("port", "p", 1337, "Port to listen on for connections")
|
||||
rootCmd.Flags().StringP("address", "a", "0.0.0.0", "Address to listen on for connections")
|
||||
rootCmd.Flags().Bool("insecure", false, "Skip client certificate verification")
|
||||
rootCmd.Flags().String("ca", "ca.crt", "Path to the CA cert")
|
||||
rootCmd.Flags().String("cert", "portal.crt", "Path to the server cert")
|
||||
rootCmd.Flags().String("key", "portal.key", "Path to the server key")
|
||||
|
||||
viper.BindPFlag("tls.insecure", rootCmd.Flags().Lookup("insecure"))
|
||||
viper.BindPFlag("tls.ca", rootCmd.Flags().Lookup("ca"))
|
||||
viper.BindPFlag("tls.cert", rootCmd.Flags().Lookup("cert"))
|
||||
viper.BindPFlag("tls.key", rootCmd.Flags().Lookup("key"))
|
||||
viper.BindPFlag("logging.loglevel", rootCmd.PersistentFlags().Lookup("loglevel"))
|
||||
viper.BindPFlag("logging.json", rootCmd.PersistentFlags().Lookup("json"))
|
||||
viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
|
||||
viper.BindPFlag("address", rootCmd.Flags().Lookup("address"))
|
||||
|
||||
rootCmd.DisableSuggestions = false
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
viper.SetConfigFile(cfgFile)
|
||||
viper.AutomaticEnv()
|
||||
|
||||
json := viper.GetBool("logging.json")
|
||||
if json {
|
||||
handler := jsonhandler.New(os.Stdout)
|
||||
log.SetHandler(handler)
|
||||
} else {
|
||||
handler := texthandler.New(os.Stdout)
|
||||
log.SetHandler(handler)
|
||||
}
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Warnf("Couldn't read config at \"%s\", starting with default settings", viper.ConfigFileUsed())
|
||||
}
|
||||
|
||||
lvl, err := log.ParseLevel(viper.GetString("logging.loglevel"))
|
||||
if err != nil {
|
||||
log.Fatalf("couldn't parse log level: %s (%s)", err, lvl)
|
||||
return
|
||||
}
|
||||
log.SetLevel(lvl)
|
||||
}
|
||||
9
cmd/portal/main.go
Normal file
9
cmd/portal/main.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/speedrunsh/speedrun/cmd/portal/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cli.Execute()
|
||||
}
|
||||
72
cmd/root.go
72
cmd/root.go
@@ -1,72 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/apex/log"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
var version string
|
||||
var commit string
|
||||
var date string
|
||||
|
||||
//Execute runs the root command
|
||||
func Execute() {
|
||||
// cobra.OnInitialize(initConfig)
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "speedrun",
|
||||
Short: "Cloud first command execution",
|
||||
Version: fmt.Sprintf("%s, commit: %s, date: %s", version, commit, date),
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
}
|
||||
|
||||
rootCmd.AddCommand(initCmd)
|
||||
rootCmd.AddCommand(keyCmd)
|
||||
rootCmd.AddCommand(runCmd)
|
||||
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
dir := filepath.Join(home, ".speedrun")
|
||||
path := filepath.Join(dir, "config.toml")
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", path, "config file")
|
||||
rootCmd.PersistentFlags().StringP("loglevel", "l", "info", "Log level")
|
||||
viper.BindPFlag("loglevel", rootCmd.PersistentFlags().Lookup("loglevel"))
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
dir, file := filepath.Split(cfgFile)
|
||||
viper.SetConfigName(file)
|
||||
viper.SetConfigType("toml")
|
||||
viper.AddConfigPath(dir)
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
log.Fatal("Run `speedrun init` first")
|
||||
}
|
||||
} else {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
lvl, err := log.ParseLevel(viper.GetString("loglevel"))
|
||||
if err != nil {
|
||||
log.Fatalf("Couldn't parse log level: %s", err)
|
||||
return
|
||||
}
|
||||
log.SetLevel(lvl)
|
||||
|
||||
}
|
||||
101
cmd/run.go
101
cmd/run.go
@@ -1,101 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"speedrun/cloud"
|
||||
"speedrun/key"
|
||||
"speedrun/marathon"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var runCmd = &cobra.Command{
|
||||
Use: "run <command to run>",
|
||||
Short: "Run command on remote servers",
|
||||
Example: " speedrun run whoami\n speedrun run whoami --only-failures --target \"labels.foo = bar AND labels.environment = staging\"",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
initConfig()
|
||||
},
|
||||
RunE: run,
|
||||
}
|
||||
|
||||
func init() {
|
||||
runCmd.Flags().StringP("target", "t", "", "Fetch instances that match the target selection criteria")
|
||||
runCmd.Flags().String("projectid", "", "Override GCP project id")
|
||||
runCmd.Flags().Bool("only-failures", false, "Print only failures and errors")
|
||||
runCmd.Flags().Bool("ignore-fingerprint", false, "Ignore host's fingerprint mismatch")
|
||||
runCmd.Flags().Duration("timeout", time.Duration(10*time.Second), "SSH connection timeout")
|
||||
runCmd.Flags().Int("concurrency", 100, "Number of maximum concurrent SSH workers")
|
||||
runCmd.Flags().Bool("use-private-ip", false, "Connect to private IPs instead of public ones")
|
||||
runCmd.Flags().Bool("use-oslogin", false, "Authenticate via OS Login")
|
||||
viper.BindPFlag("gcp.projectid", runCmd.Flags().Lookup("projectid"))
|
||||
viper.BindPFlag("gcp.use-oslogin", runCmd.Flags().Lookup("use-oslogin"))
|
||||
viper.BindPFlag("ssh.timeout", runCmd.Flags().Lookup("timeout"))
|
||||
viper.BindPFlag("ssh.ignore-fingerprint", runCmd.Flags().Lookup("ignore-fingerprint"))
|
||||
viper.BindPFlag("ssh.only-failures", runCmd.Flags().Lookup("only-failures"))
|
||||
viper.BindPFlag("ssh.concurrency", runCmd.Flags().Lookup("concurrency"))
|
||||
viper.BindPFlag("ssh.use-private-ip", runCmd.Flags().Lookup("use-private-ip"))
|
||||
}
|
||||
|
||||
func run(cmd *cobra.Command, args []string) error {
|
||||
command := strings.Join(args, " ")
|
||||
project := viper.GetString("gcp.projectid")
|
||||
timeout := viper.GetDuration("ssh.timeout")
|
||||
ignoreFingerprint := viper.GetBool("ssh.ignore-fingerprint")
|
||||
onlyFailures := viper.GetBool("ssh.only-failures")
|
||||
concurrency := viper.GetInt("ssh.concurrency")
|
||||
usePrivateIP := viper.GetBool("ssh.use-private-ip")
|
||||
useOSlogin := viper.GetBool("gcp.use-oslogin")
|
||||
|
||||
target, err := cmd.Flags().GetString("target")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gcpClient, err := cloud.NewGCPClient(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path, err := determineKeyFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k, err := key.Read(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("Fetching instance list")
|
||||
instances, err := gcpClient.GetInstances(target, usePrivateIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(instances) == 0 {
|
||||
log.Warn("No instances found")
|
||||
return nil
|
||||
}
|
||||
|
||||
if useOSlogin {
|
||||
user, err := gcpClient.GetSAUsername()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
k.User = user
|
||||
}
|
||||
|
||||
m := marathon.New(command, timeout, concurrency)
|
||||
err = m.Run(instances, k, ignoreFingerprint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.PrintResult(onlyFailures)
|
||||
return nil
|
||||
}
|
||||
61
cmd/speedrun/cli/init.go
Normal file
61
cmd/speedrun/cli/init.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/pelletier/go-toml"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initCmd.SetUsageTemplate(usage)
|
||||
initCmd.Flags().BoolP("print", "p", false, "Print default config to stdout")
|
||||
}
|
||||
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize speedrun",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
viper.SetDefault("gcp.projectid", "")
|
||||
print, err := cmd.Flags().GetBool("print")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if print {
|
||||
c := viper.AllSettings()
|
||||
bs, err := toml.Marshal(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal config: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(bs))
|
||||
return nil
|
||||
}
|
||||
|
||||
dir := filepath.Dir(cfgFile)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err = os.Mkdir(dir, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = viper.SafeWriteConfigAs(cfgFile)
|
||||
if err != nil {
|
||||
if _, ok := err.(viper.ConfigFileAlreadyExistsError); !ok {
|
||||
return fmt.Errorf("couldn't save config at \"%s\" (%s)", viper.ConfigFileUsed(), err)
|
||||
}
|
||||
} else {
|
||||
log.Infof("Your config was saved at \"%s\"", viper.ConfigFileUsed())
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("Config already exists at \"%s\", no changes applied", viper.ConfigFileUsed())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
97
cmd/speedrun/cli/root.go
Normal file
97
cmd/speedrun/cli/root.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/apex/log"
|
||||
jsonhandler "github.com/apex/log/handlers/json"
|
||||
texthandler "github.com/apex/log/handlers/text"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
var version string
|
||||
var commit string
|
||||
var date string
|
||||
|
||||
//go:embed templates/root.tmpl
|
||||
var rootUsage string
|
||||
|
||||
//go:embed templates/usage.tmpl
|
||||
var usage string
|
||||
|
||||
func Execute() {
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "speedrun",
|
||||
Short: "Control your compute fleet at scale",
|
||||
Version: fmt.Sprintf("%s, commit: %s, date: %s", version, commit, date),
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
}
|
||||
|
||||
cobra.OnInitialize(initConfig)
|
||||
rootCmd.SetUsageTemplate(rootUsage)
|
||||
rootCmd.AddCommand(initCmd, runCmd, serviceCmd)
|
||||
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
dir := filepath.Join(home, ".speedrun")
|
||||
configPath := filepath.Join(dir, "config.toml")
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", configPath, "config file")
|
||||
rootCmd.PersistentFlags().StringP("loglevel", "l", "info", "Log level")
|
||||
rootCmd.PersistentFlags().BoolP("json", "j", false, "Output logs in JSON format")
|
||||
rootCmd.PersistentFlags().StringP("target", "t", "", "Fetch instances that match the target selection criteria")
|
||||
rootCmd.PersistentFlags().Bool("insecure", false, "Skip server certificate verification")
|
||||
rootCmd.PersistentFlags().String("ca", "ca.crt", "Path to the CA cert")
|
||||
rootCmd.PersistentFlags().String("cert", "cert.crt", "Path to the client cert")
|
||||
rootCmd.PersistentFlags().String("key", "key.key", "Path to the client key")
|
||||
rootCmd.PersistentFlags().Bool("use-private-ip", false, "Connect to private IPs instead of public ones")
|
||||
|
||||
viper.BindPFlag("logging.loglevel", rootCmd.PersistentFlags().Lookup("loglevel"))
|
||||
viper.BindPFlag("logging.json", rootCmd.PersistentFlags().Lookup("json"))
|
||||
viper.BindPFlag("tls.insecure", rootCmd.PersistentFlags().Lookup("insecure"))
|
||||
viper.BindPFlag("tls.ca", rootCmd.PersistentFlags().Lookup("ca"))
|
||||
viper.BindPFlag("tls.cert", rootCmd.PersistentFlags().Lookup("cert"))
|
||||
viper.BindPFlag("tls.key", rootCmd.PersistentFlags().Lookup("key"))
|
||||
viper.BindPFlag("portal.use-private-ip", rootCmd.PersistentFlags().Lookup("use-private-ip"))
|
||||
|
||||
rootCmd.DisableSuggestions = false
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
viper.SetConfigFile(cfgFile)
|
||||
viper.AutomaticEnv()
|
||||
|
||||
json := viper.GetBool("logging.json")
|
||||
if json {
|
||||
handler := jsonhandler.New(os.Stdout)
|
||||
log.SetHandler(handler)
|
||||
} else {
|
||||
handler := texthandler.New(os.Stdout)
|
||||
log.SetHandler(handler)
|
||||
}
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Warnf("Couldn't read config at \"%s\", starting with default settings", viper.ConfigFileUsed())
|
||||
}
|
||||
|
||||
lvl, err := log.ParseLevel(viper.GetString("logging.loglevel"))
|
||||
if err != nil {
|
||||
log.Fatalf("couldn't parse log level: %s (%s)", err, lvl)
|
||||
return
|
||||
}
|
||||
log.SetLevel(lvl)
|
||||
}
|
||||
86
cmd/speedrun/cli/run.go
Normal file
86
cmd/speedrun/cli/run.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alitto/pond"
|
||||
"github.com/speedrunsh/speedrun/pkg/speedrun/cloud"
|
||||
portalpb "github.com/speedrunsh/speedrun/proto/portal"
|
||||
"storj.io/drpc/drpcconn"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var runCmd = &cobra.Command{
|
||||
Use: "run <command to run>",
|
||||
Short: "Run a shell command on remote servers",
|
||||
Example: " speedrun run whoami\n speedrun run whoami --target \"labels.foo = bar AND labels.environment = staging\"",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: run,
|
||||
}
|
||||
|
||||
func init() {
|
||||
runCmd.SetUsageTemplate(usage)
|
||||
}
|
||||
|
||||
func run(cmd *cobra.Command, args []string) error {
|
||||
command := strings.Join(args, " ")
|
||||
s := strings.Split(command, " ")
|
||||
usePrivateIP := viper.GetBool("portal.use-private-ip")
|
||||
|
||||
tlsConfig, err := cloud.SetupTLS()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target, err := cmd.Flags().GetString("target")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
instances, err := cloud.GetInstances(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pool := pond.New(1000, 10000)
|
||||
for _, p := range instances {
|
||||
instance := p
|
||||
pool.Submit(func() {
|
||||
fields := log.Fields{
|
||||
"host": instance.Name,
|
||||
"address": instance.GetAddress(usePrivateIP),
|
||||
}
|
||||
log := log.WithFields(fields)
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", instance.GetAddress(usePrivateIP), 1337)
|
||||
rawconn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
conn := drpcconn.New(rawconn)
|
||||
defer conn.Close()
|
||||
|
||||
c := portalpb.NewDRPCPortalClient(conn)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
r, err := c.RunCommand(ctx, &portalpb.CommandRequest{Name: s[0], Args: s[1:]})
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
log.WithField("state", r.GetState()).Info(r.GetMessage())
|
||||
})
|
||||
}
|
||||
pool.StopAndWait()
|
||||
return nil
|
||||
}
|
||||
142
cmd/speedrun/cli/service.go
Normal file
142
cmd/speedrun/cli/service.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alitto/pond"
|
||||
"github.com/apex/log"
|
||||
"github.com/speedrunsh/speedrun/pkg/speedrun/cloud"
|
||||
portalpb "github.com/speedrunsh/speedrun/proto/portal"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"storj.io/drpc/drpcconn"
|
||||
)
|
||||
|
||||
var serviceCmd = &cobra.Command{
|
||||
Use: "service",
|
||||
Short: "Manage services",
|
||||
TraverseChildren: true,
|
||||
}
|
||||
|
||||
var restartCmd = &cobra.Command{
|
||||
Use: "restart <servicename>",
|
||||
Short: "Restart a service",
|
||||
Example: " speedrun service restart nginx",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: action,
|
||||
}
|
||||
|
||||
var startCmd = &cobra.Command{
|
||||
Use: "start <servicename>",
|
||||
Short: "Start a service",
|
||||
Example: " speedrun service start nginx",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: action,
|
||||
}
|
||||
|
||||
var stopCmd = &cobra.Command{
|
||||
Use: "stop <servicename>",
|
||||
Short: "Stop a service",
|
||||
Example: " speedrun service stop nginx",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: action,
|
||||
}
|
||||
|
||||
var statusCmd = &cobra.Command{
|
||||
Use: "status <servicename>",
|
||||
Short: "Return the status of the service",
|
||||
Example: " speedrun service status nginx",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: action,
|
||||
}
|
||||
|
||||
func init() {
|
||||
serviceCmd.SetUsageTemplate(usage)
|
||||
serviceCmd.AddCommand(restartCmd)
|
||||
serviceCmd.AddCommand(startCmd)
|
||||
serviceCmd.AddCommand(stopCmd)
|
||||
serviceCmd.AddCommand(statusCmd)
|
||||
}
|
||||
|
||||
func action(cmd *cobra.Command, args []string) error {
|
||||
usePrivateIP := viper.GetBool("portal.use-private-ip")
|
||||
|
||||
tlsConfig, err := cloud.SetupTLS()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target, err := cmd.Flags().GetString("target")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
portals, err := cloud.GetInstances(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pool := pond.New(1000, 10000)
|
||||
for _, p := range portals {
|
||||
portal := p
|
||||
pool.Submit(func() {
|
||||
fields := log.Fields{
|
||||
"host": portal.Name,
|
||||
"address": portal.GetAddress(usePrivateIP),
|
||||
}
|
||||
log := log.WithFields(fields)
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", portal.GetAddress(usePrivateIP), 1337)
|
||||
rawconn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
conn := drpcconn.New(rawconn)
|
||||
defer conn.Close()
|
||||
|
||||
c := portalpb.NewDRPCPortalClient(conn)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
switch cmd.Name() {
|
||||
case "restart":
|
||||
r, err := c.ServiceRestart(ctx, &portalpb.ServiceRequest{Name: strings.Join(args, " ")})
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
log.WithField("state", r.GetState()).Info(r.GetMessage())
|
||||
case "start":
|
||||
r, err := c.ServiceStart(ctx, &portalpb.ServiceRequest{Name: strings.Join(args, " ")})
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
log.WithField("state", r.GetState()).Info(r.GetMessage())
|
||||
case "stop":
|
||||
r, err := c.ServiceStop(ctx, &portalpb.ServiceRequest{Name: strings.Join(args, " ")})
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
log.WithField("state", r.GetState()).Info(r.GetMessage())
|
||||
case "status":
|
||||
r, err := c.ServiceStatus(ctx, &portalpb.ServiceRequest{Name: strings.Join(args, " ")})
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
log.WithField("state", r.GetState()).Infof("Loadstate: \"%s\", Activestate: \"%s\", Substate: \"%s\"", r.GetLoadstate(), r.GetActivestate(), r.GetSubstate())
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
pool.StopAndWait()
|
||||
return nil
|
||||
}
|
||||
18
cmd/speedrun/cli/templates/root.tmpl
Normal file
18
cmd/speedrun/cli/templates/root.tmpl
Normal file
@@ -0,0 +1,18 @@
|
||||
Usage:{{if .Runnable}}
|
||||
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
|
||||
{{.CommandPath}} [command]{{end}}
|
||||
|
||||
Core Commands:{{range .Commands}}{{if (or (eq .Name "help") (eq .Name "init") (eq .Name "key") (eq .Name "portal") (eq .Name "completion"))}}
|
||||
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}
|
||||
|
||||
Action Commands:{{range .Commands}}{{if (or (eq .Name "run") (eq .Name "exec") (eq .Name "service"))}}
|
||||
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}
|
||||
{{if .HasAvailableLocalFlags}}
|
||||
Flags:
|
||||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
||||
|
||||
Global Flags:
|
||||
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
|
||||
|
||||
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
|
||||
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}
|
||||
23
cmd/speedrun/cli/templates/usage.tmpl
Normal file
23
cmd/speedrun/cli/templates/usage.tmpl
Normal file
@@ -0,0 +1,23 @@
|
||||
Usage:{{if .Runnable}}
|
||||
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
|
||||
{{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
|
||||
|
||||
Aliases:
|
||||
{{.NameAndAliases}}{{end}}{{if .HasExample}}
|
||||
|
||||
Examples:
|
||||
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
|
||||
|
||||
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
|
||||
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
||||
|
||||
Flags:
|
||||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
||||
|
||||
Global Flags:
|
||||
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
|
||||
|
||||
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
|
||||
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
|
||||
|
||||
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
|
||||
9
cmd/speedrun/main.go
Normal file
9
cmd/speedrun/main.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/speedrunsh/speedrun/cmd/speedrun/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cli.Execute()
|
||||
}
|
||||
Reference in New Issue
Block a user