203
cmd/key.go
203
cmd/key.go
@@ -1,22 +1,15 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
gcp "speedrun/cloud"
|
||||
"speedrun/cloud"
|
||||
"speedrun/key"
|
||||
|
||||
"github.com/alitto/pond"
|
||||
"github.com/apex/log"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var keyCmd = &cobra.Command{
|
||||
@@ -34,32 +27,43 @@ var newKeyCmd = &cobra.Command{
|
||||
RunE: newKey,
|
||||
}
|
||||
|
||||
var setKeyCmd = &cobra.Command{
|
||||
Use: "set",
|
||||
Short: "Set key in the project or instance metadata",
|
||||
Example: " speedrun key set \n speedrun key set --filter \"labels.foo = bar AND labels.environment = staging\"",
|
||||
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: setKey,
|
||||
RunE: authorizeKey,
|
||||
}
|
||||
|
||||
var removeKeyCmd = &cobra.Command{
|
||||
Use: "remove",
|
||||
Short: "Remove key from the project metadata or instance metadata",
|
||||
Example: " speedrun key remove \n speedrun key remove --filter \"labels.foo = bar AND labels.environment = staging\"",
|
||||
var revokeKeyCmd = &cobra.Command{
|
||||
Use: "revoke",
|
||||
Short: "Revoke ssh key",
|
||||
Example: " speedrun key revoke",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
initConfig()
|
||||
},
|
||||
RunE: removeKey,
|
||||
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() {
|
||||
setKeyCmd.Flags().String("filter", "", "Set the key only on matching instances")
|
||||
removeKeyCmd.Flags().String("filter", "", "Set the key only on matching instances")
|
||||
keyCmd.AddCommand(newKeyCmd)
|
||||
keyCmd.AddCommand(setKeyCmd)
|
||||
keyCmd.AddCommand(removeKeyCmd)
|
||||
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) {
|
||||
@@ -74,24 +78,17 @@ func determineKeyFilePath() (string, error) {
|
||||
}
|
||||
|
||||
func newKey(cmd *cobra.Command, args []string) error {
|
||||
log.Debug("Generating new private key")
|
||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
k, err := key.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("Converting private key to PKCS8 format")
|
||||
pemBlock := &pem.Block{}
|
||||
pemBlock.Type = "PRIVATE KEY"
|
||||
pemBlock.Bytes, err = x509.MarshalPKCS8PrivateKey(privKey)
|
||||
path, err := determineKeyFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("Encoding the key to PEM format")
|
||||
privateKey := pem.EncodeToMemory(pemBlock)
|
||||
|
||||
err = writeKeyFile(privateKey)
|
||||
err = k.Write(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -100,142 +97,84 @@ func newKey(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeKeyFile(key []byte) error {
|
||||
privateKeyPath, err := determineKeyFilePath()
|
||||
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
|
||||
}
|
||||
|
||||
log.Debugf("Writing priviate key to %s", privateKeyPath)
|
||||
err = ioutil.WriteFile(privateKeyPath, key, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadKeyPair() (ssh.PublicKey, ssh.Signer, error) {
|
||||
privateKeyPath, err := determineKeyFilePath()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
file, err := readKeyFile(privateKeyPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("couldn't find private key. Use 'speedrun key new' to generate a new one")
|
||||
}
|
||||
|
||||
signer, err := ssh.ParsePrivateKey(file)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pubKey := signer.PublicKey()
|
||||
return pubKey, signer, nil
|
||||
}
|
||||
|
||||
func readKeyFile(path string) ([]byte, error) {
|
||||
cleanPath := filepath.Clean(path)
|
||||
absPath, err := filepath.Abs(cleanPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := ioutil.ReadFile(absPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func setKey(cmd *cobra.Command, args []string) error {
|
||||
client, err := gcp.NewComputeClient(viper.GetString("gcp.projectid"))
|
||||
path, err := determineKeyFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pubKey, _, err := loadKeyPair()
|
||||
k, err := key.Read(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filter, err := cmd.Flags().GetString("filter")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if filter != "" {
|
||||
log.Info("Setting public key in the instance metadata")
|
||||
instances, err := client.GetInstances(filter)
|
||||
if useOSlogin {
|
||||
gcpClient.AddUserKey(k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(instances) == 0 {
|
||||
log.Warn("no instances found")
|
||||
}
|
||||
|
||||
pool := pond.New(10, 0, pond.MinWorkers(10))
|
||||
for i := 0; i < len(instances); i++ {
|
||||
n := i
|
||||
pool.Submit(func() {
|
||||
client.AddKeyToMetadata(instances[n], pubKey)
|
||||
})
|
||||
}
|
||||
|
||||
pool.StopAndWait()
|
||||
log.Info("Authorized key via OS Login")
|
||||
} else {
|
||||
log.Info("Setting public key in the project metadata")
|
||||
err = client.AddKeyToMetadataP(pubKey)
|
||||
gcpClient.AddKeyToMetadata(k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("Authorized key in the project metadata")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeKey(cmd *cobra.Command, args []string) error {
|
||||
client, err := gcp.NewComputeClient(viper.GetString("gcp.projectid"))
|
||||
func revokeKey(cmd *cobra.Command, args []string) error {
|
||||
project := viper.GetString("gcp.projectid")
|
||||
gcpClient, err := cloud.NewGCPClient(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pubKey, _, err := loadKeyPair()
|
||||
path, err := determineKeyFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filter, err := cmd.Flags().GetString("filter")
|
||||
k, err := key.Read(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if filter != "" {
|
||||
log.Info("Removing public from the instance metadata")
|
||||
instances, err := client.GetInstances(filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("Revoking public key")
|
||||
err = gcpClient.RemoveKeyFromMetadata(k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(instances) == 0 {
|
||||
log.Warn("no instances found")
|
||||
}
|
||||
|
||||
pool := pond.New(10, 0, pond.MinWorkers(10))
|
||||
for i := 0; i < len(instances); i++ {
|
||||
n := i
|
||||
pool.Submit(func() {
|
||||
client.RemoveKeyFromMetadata(instances[n], pubKey)
|
||||
})
|
||||
}
|
||||
|
||||
pool.StopAndWait()
|
||||
} else {
|
||||
log.Info("Removing public key from the project metadata")
|
||||
err = client.RemoveKeyFromMetadataP(pubKey)
|
||||
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
|
||||
|
||||
42
cmd/run.go
42
cmd/run.go
@@ -1,7 +1,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
gcp "speedrun/cloud"
|
||||
"speedrun/cloud"
|
||||
"speedrun/key"
|
||||
"speedrun/marathon"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -30,7 +31,9 @@ func init() {
|
||||
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"))
|
||||
@@ -41,48 +44,55 @@ func init() {
|
||||
|
||||
func run(cmd *cobra.Command, args []string) error {
|
||||
command := strings.Join(args, " ")
|
||||
projectid := viper.GetString("gcp.projectid")
|
||||
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")
|
||||
|
||||
filter, err := cmd.Flags().GetString("filter")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := gcp.NewComputeClient(projectid)
|
||||
gcpClient, err := cloud.NewGCPClient(project)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
privateKeyPath, err := determineKeyFilePath()
|
||||
path, err := determineKeyFilePath()
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
k, err := key.Read(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("Fetching list of GCE instances")
|
||||
instances, err := client.GetInstances(filter)
|
||||
instances, err := gcpClient.GetInstances(filter, usePrivateIP)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if len(instances) == 0 {
|
||||
log.Warn("No instances found")
|
||||
return nil
|
||||
}
|
||||
|
||||
m := marathon.New(command, timeout, concurrency)
|
||||
instanceDict := map[string]string{}
|
||||
for _, instance := range instances {
|
||||
if usePrivateIP {
|
||||
instanceDict[instance.NetworkInterfaces[0].NetworkIP] = instance.Name
|
||||
} else {
|
||||
instanceDict[instance.NetworkInterfaces[0].AccessConfigs[0].NatIP] = instance.Name
|
||||
if useOSlogin {
|
||||
user, err := gcpClient.GetSAUsername()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
k.User = user
|
||||
}
|
||||
|
||||
err = m.Run(instanceDict, privateKeyPath, ignoreFingerprint)
|
||||
m := marathon.New(command, timeout, concurrency)
|
||||
err = m.Run(instances, k, ignoreFingerprint)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user