Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Environment subcommands #89

Closed
wants to merge 14 commits into from
Next Next commit
WIP mirgation to urfave/cli
  • Loading branch information
cmoog committed Jul 31, 2020
commit ad0f9c96656e09860e76ae2e67313d8a11ffd7e1
209 changes: 106 additions & 103 deletions cmd/coder/configssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,162 +11,165 @@ import (
"strings"
"time"

"github.com/spf13/pflag"
"go.coder.com/cli"
"go.coder.com/flog"

"cdr.dev/coder-cli/internal/config"
"cdr.dev/coder-cli/internal/entclient"
)
"github.com/urfave/cli"

var (
privateKeyFilepath = filepath.Join(os.Getenv("HOME"), ".ssh", "coder_enterprise")
"go.coder.com/flog"
)

type configSSHCmd struct {
filepath string
remove bool

startToken, startMessage, endToken string
}

func (cmd *configSSHCmd) Spec() cli.CommandSpec {
return cli.CommandSpec{
Name: "config-ssh",
Usage: "",
Desc: "add your Coder Enterprise environments to ~/.ssh/config",
func makeConfigSSHCmd() cli.Command {
var (
configpath string
remove = false
)

return cli.Command{
Name: "config-ssh",
UsageText: "",
Description: "add your Coder Enterprise environments to ~/.ssh/config",
Action: configSSH(&configpath, &remove),
Flags: []cli.Flag{
cli.StringFlag{
Name: "filepath",
Usage: "overide the default path of your ssh config file",
Value: filepath.Join(os.Getenv("HOME"), ".ssh", "config"),
TakesFile: true,
Destination: &configpath,
},
cli.BoolFlag{
Name: "remove",
Usage: "remove the auto-generated Coder Enterprise ssh config",
Destination: &remove,
},
},
}
}

func (cmd *configSSHCmd) RegisterFlags(fl *pflag.FlagSet) {
fl.BoolVar(&cmd.remove, "remove", false, "remove the auto-generated Coder Enterprise ssh config")
home := os.Getenv("HOME")
defaultPath := filepath.Join(home, ".ssh", "config")
fl.StringVar(&cmd.filepath, "config-path", defaultPath, "overide the default path of your ssh config file")

cmd.startToken = "# ------------START-CODER-ENTERPRISE-----------"
cmd.startMessage = `# The following has been auto-generated by "coder config-ssh"
func configSSH(filepath *string, remove *bool) func(c *cli.Context) {
startToken := "# ------------START-CODER-ENTERPRISE-----------"
startMessage := `# The following has been auto-generated by "coder config-ssh"
# to make accessing your Coder Enterprise environments easier.
#
# To remove this blob, run:
#
# coder config-ssh --remove
#
# You should not hand-edit this section, unless you are deleting it.`
cmd.endToken = "# ------------END-CODER-ENTERPRISE------------"
}

func (cmd *configSSHCmd) Run(fl *pflag.FlagSet) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
endToken := "# ------------END-CODER-ENTERPRISE------------"

return func(c *cli.Context) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

currentConfig, err := readStr(*filepath)
if os.IsNotExist(err) {
// SSH configs are not always already there.
currentConfig = ""
} else if err != nil {
flog.Fatal("failed to read ssh config file %q: %v", filepath, err)
}

currentConfig, err := readStr(cmd.filepath)
if os.IsNotExist(err) {
// SSH configs are not always already there.
currentConfig = ""
} else if err != nil {
flog.Fatal("failed to read ssh config file %q: %v", cmd.filepath, err)
}
startIndex := strings.Index(currentConfig, startToken)
endIndex := strings.Index(currentConfig, endToken)

startIndex := strings.Index(currentConfig, cmd.startToken)
endIndex := strings.Index(currentConfig, cmd.endToken)
if *remove {
if startIndex == -1 || endIndex == -1 {
flog.Fatal("the Coder Enterprise ssh configuration section could not be safely deleted or does not exist")
}
currentConfig = currentConfig[:startIndex-1] + currentConfig[endIndex+len(endToken)+1:]

if cmd.remove {
if startIndex == -1 || endIndex == -1 {
flog.Fatal("the Coder Enterprise ssh configuration section could not be safely deleted or does not exist")
}
currentConfig = currentConfig[:startIndex-1] + currentConfig[endIndex+len(cmd.endToken)+1:]
err = writeStr(*filepath, currentConfig)
if err != nil {
flog.Fatal("failed to write to ssh config file %q: %v", *filepath, err)
}

err = writeStr(cmd.filepath, currentConfig)
if err != nil {
flog.Fatal("failed to write to ssh config file %q: %v", cmd.filepath, err)
return
}

return
}
entClient := requireAuth()

entClient := requireAuth()
sshAvailable := isSSHAvailable(ctx)
if !sshAvailable {
flog.Fatal("SSH is disabled or not available for your Coder Enterprise deployment.")
}

sshAvailable := cmd.ensureSSHAvailable(ctx)
if !sshAvailable {
flog.Fatal("SSH is disabled or not available for your Coder Enterprise deployment.")
}
me, err := entClient.Me()
if err != nil {
flog.Fatal("failed to fetch username: %v", err)
}

me, err := entClient.Me()
if err != nil {
flog.Fatal("failed to fetch username: %v", err)
}
envs := getEnvs(entClient)
if len(envs) < 1 {
flog.Fatal("no environments found")
}
newConfig, err := makeNewConfigs(me.Username, envs, startToken, startMessage, endToken)
if err != nil {
flog.Fatal("failed to make new ssh configurations: %v", err)
}

envs := getEnvs(entClient)
if len(envs) < 1 {
flog.Fatal("no environments found")
}
newConfig, err := cmd.makeNewConfigs(me.Username, envs)
if err != nil {
flog.Fatal("failed to make new ssh configurations: %v", err)
}
// if we find the old config, remove those chars from the string
if startIndex != -1 && endIndex != -1 {
currentConfig = currentConfig[:startIndex-1] + currentConfig[endIndex+len(endToken)+1:]
}

// if we find the old config, remove those chars from the string
if startIndex != -1 && endIndex != -1 {
currentConfig = currentConfig[:startIndex-1] + currentConfig[endIndex+len(cmd.endToken)+1:]
}
err = writeStr(*filepath, currentConfig+newConfig)
if err != nil {
flog.Fatal("failed to write new configurations to ssh config file %q: %v", filepath, err)
}
err = writeSSHKey(ctx, entClient)
if err != nil {
flog.Fatal("failed to fetch and write ssh key: %v", err)
}

err = writeStr(cmd.filepath, currentConfig+newConfig)
if err != nil {
flog.Fatal("failed to write new configurations to ssh config file %q: %v", cmd.filepath, err)
}
err = writeSSHKey(ctx, entClient)
if err != nil {
flog.Fatal("failed to fetch and write ssh key: %v", err)
fmt.Printf("An auto-generated ssh config was written to %q\n", filepath)
fmt.Printf("Your private ssh key was written to %q\n", privateKeyFilepath)
fmt.Println("You should now be able to ssh into your environment")
fmt.Printf("For example, try running\n\n\t$ ssh coder.%s\n\n", envs[0].Name)
}

fmt.Printf("An auto-generated ssh config was written to %q\n", cmd.filepath)
fmt.Printf("Your private ssh key was written to %q\n", privateKeyFilepath)
fmt.Println("You should now be able to ssh into your environment")
fmt.Printf("For example, try running\n\n\t$ ssh coder.%s\n\n", envs[0].Name)
}

var (
privateKeyFilepath = filepath.Join(os.Getenv("HOME"), ".ssh", "coder_enterprise")
)

func writeSSHKey(ctx context.Context, client *entclient.Client) error {
key, err := client.SSHKey()
if err != nil {
return err
}
err = ioutil.WriteFile(privateKeyFilepath, []byte(key.PrivateKey), 0400)
if err != nil {
return err
}
return nil
return ioutil.WriteFile(privateKeyFilepath, []byte(key.PrivateKey), 0400)
}

func (cmd *configSSHCmd) makeNewConfigs(userName string, envs []entclient.Environment) (string, error) {
func makeNewConfigs(userName string, envs []entclient.Environment, startToken, startMsg, endToken string) (string, error) {
hostname, err := configuredHostname()
if err != nil {
return "", nil
}

newConfig := fmt.Sprintf("\n%s\n%s\n\n", cmd.startToken, cmd.startMessage)
newConfig := fmt.Sprintf("\n%s\n%s\n\n", startToken, startMsg)
for _, env := range envs {
newConfig += cmd.makeConfig(hostname, userName, env.Name)
newConfig += makeSSHConfig(hostname, userName, env.Name)
}
newConfig += fmt.Sprintf("\n%s\n", cmd.endToken)
newConfig += fmt.Sprintf("\n%s\n", endToken)

return newConfig, nil
}

func (cmd *configSSHCmd) makeConfig(host, userName, envName string) string {
func makeSSHConfig(host, userName, envName string) string {
return fmt.Sprintf(
`Host coder.%s
HostName %s
User %s-%s
StrictHostKeyChecking no
ConnectTimeout=0
IdentityFile=%s
ServerAliveInterval 60
ServerAliveCountMax 3
HostName %s
User %s-%s
StrictHostKeyChecking no
ConnectTimeout=0
IdentityFile=%s
ServerAliveInterval 60
ServerAliveCountMax 3
`, envName, host, userName, envName, privateKeyFilepath)
}

func (cmd *configSSHCmd) ensureSSHAvailable(ctx context.Context) bool {
func isSSHAvailable(ctx context.Context) bool {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

Expand Down
30 changes: 14 additions & 16 deletions cmd/coder/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,28 @@ import (
"strings"
"sync"

"cdr.dev/coder-cli/internal/config"
"cdr.dev/coder-cli/internal/loginsrv"
"github.com/pkg/browser"
"github.com/spf13/pflag"
"github.com/urfave/cli"

"go.coder.com/cli"
"go.coder.com/flog"

"cdr.dev/coder-cli/internal/config"
"cdr.dev/coder-cli/internal/loginsrv"
)

type loginCmd struct {
}

func (cmd loginCmd) Spec() cli.CommandSpec {
return cli.CommandSpec{
Name: "login",
Usage: "[Coder Enterprise URL eg. http://my.coder.domain/ ]",
Desc: "authenticate this client for future operations",
func makeLoginCmd() cli.Command {
cmd := cli.Command{
Name: "login",
Usage: "[Coder Enterprise URL eg. http://my.coder.domain/ ]",
Description: "authenticate this client for future operations",
Action: login,
}
return cmd
}
func (cmd loginCmd) Run(fl *pflag.FlagSet) {
rawURL := fl.Arg(0)

func login(c *cli.Context) {
rawURL := c.Args().First()
if rawURL == "" || !strings.HasPrefix(rawURL, "http") {
exitUsage(fl)
flog.Fatal("invalid URL")
}

u, err := url.Parse(rawURL)
Expand Down
19 changes: 8 additions & 11 deletions cmd/coder/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,22 @@ package main
import (
"os"

"github.com/spf13/pflag"
"cdr.dev/coder-cli/internal/config"
"github.com/urfave/cli"

"go.coder.com/cli"
"go.coder.com/flog"

"cdr.dev/coder-cli/internal/config"
)

type logoutCmd struct {
}

func (cmd logoutCmd) Spec() cli.CommandSpec {
return cli.CommandSpec{
func makeLogoutCmd() cli.Command {
return cli.Command{
Name: "logout",
Desc: "remove local authentication credentials (if any)",
//Usage: "",
Description: "remove local authentication credentials (if any)",
Action: logout,
}
}

func (cmd logoutCmd) Run(_ *pflag.FlagSet) {
func logout(c *cli.Context) {
err := config.Session.Delete()
if err != nil {
if os.IsNotExist(err) {
Expand Down
Loading