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

Commit 2d39caa

Browse files
committed
Add basic login flow
- And other structure
1 parent 02db2ee commit 2d39caa

22 files changed

+759
-0
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# coder
2+
3+
`coder` provides a one-way, live file sync to your Coder Enterprise environment.
4+
5+
It is useful in cases where you want to use an unsupported IDE with your Coder
6+
Environment.
7+
8+
## Login
9+
```shell script
10+
$ coder login https://my-coder-enterprise.com
11+
```
12+
13+
## Setting up a Sync
14+
15+
``
16+
$ coder sync ~/Projects/cdr/enterprise my-env:~/enterprise
17+
``
18+
19+
## Caveats
20+
21+
- The `coder login` flow will not work when the CLI is ran from a different network
22+
than the browser. #1

cmd/coder/exit.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package main
2+
3+
import (
4+
"os"
5+
6+
"github.com/spf13/pflag"
7+
"go.coder.com/flog"
8+
)
9+
10+
func exitOnError(err error) {
11+
if err != nil {
12+
flog.Fatal("%+v", err.Error())
13+
}
14+
}
15+
16+
func exitAfter(err error) {
17+
exitOnError(err)
18+
os.Exit(0)
19+
}
20+
21+
func exitUsage(fl *pflag.FlagSet) {
22+
fl.Usage()
23+
os.Exit(1)
24+
}

cmd/coder/login.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package main
2+
3+
import (
4+
"net"
5+
"net/http"
6+
"net/url"
7+
"sync"
8+
9+
"github.com/pkg/browser"
10+
"github.com/spf13/pflag"
11+
"go.coder.com/cli"
12+
"go.coder.com/flog"
13+
14+
"cdr.dev/coder/internal/client"
15+
"cdr.dev/coder/internal/config"
16+
"cdr.dev/coder/internal/loginsrv"
17+
)
18+
19+
type loginCmd struct {
20+
}
21+
22+
func (cmd loginCmd) Spec() cli.CommandSpec {
23+
return cli.CommandSpec{
24+
Name: "login",
25+
Usage: "[Coder Enterprise URL]",
26+
Desc: "authenticate this client for future operations",
27+
}
28+
}
29+
30+
func requireAuth() *client.Client {
31+
sessionToken, err := config.Session.Read()
32+
if err != nil {
33+
flog.Fatal("read session: %v (did you run coder login?)", err)
34+
}
35+
36+
rawURL, err := config.URL.Read()
37+
if err != nil {
38+
flog.Fatal("read url: %v (did you run coder login?)", err)
39+
}
40+
41+
u, err := url.Parse(rawURL)
42+
if err != nil {
43+
flog.Fatal("url misformatted: %v (try runing coder login)", err)
44+
}
45+
46+
return &client.Client{
47+
BaseURL: u,
48+
Token: sessionToken,
49+
}
50+
}
51+
52+
func (cmd loginCmd) Run(fl *pflag.FlagSet) {
53+
rawURL := fl.Arg(0)
54+
if rawURL == "" {
55+
exitUsage(fl)
56+
}
57+
58+
u, err := url.Parse(rawURL)
59+
if err != nil {
60+
flog.Fatal("parse url: %v", err)
61+
}
62+
63+
listener, err := net.Listen("tcp", "127.0.0.1:0")
64+
if err != nil {
65+
flog.Fatal("create login server: %+v", err)
66+
}
67+
defer listener.Close()
68+
69+
srv := &loginsrv.Server{
70+
TokenCond: sync.NewCond(&sync.Mutex{}),
71+
}
72+
go func() {
73+
_ = http.Serve(
74+
listener, srv,
75+
)
76+
}()
77+
78+
err = config.URL.Write(
79+
(&url.URL{Scheme: u.Scheme, Host: u.Host}).String(),
80+
)
81+
if err != nil {
82+
flog.Fatal("write url: %v", err)
83+
}
84+
85+
authURL := url.URL{
86+
Scheme: u.Scheme,
87+
Host: u.Host,
88+
Path: "/internal-auth/",
89+
RawQuery: "local_service=http://" + listener.Addr().String(),
90+
}
91+
92+
err = browser.OpenURL(authURL.String())
93+
if err != nil {
94+
// Tell the user to visit the URL instead.
95+
flog.Info("visit %s to login", authURL)
96+
}
97+
srv.TokenCond.L.Lock()
98+
srv.TokenCond.Wait()
99+
err = config.Session.Write(srv.Token)
100+
srv.TokenCond.L.Unlock()
101+
if err != nil {
102+
flog.Fatal("set session: %v", err)
103+
}
104+
flog.Success("logged in")
105+
}

cmd/coder/logout.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package main
2+
3+
import (
4+
"os"
5+
6+
"github.com/spf13/pflag"
7+
"go.coder.com/cli"
8+
"go.coder.com/flog"
9+
10+
"cdr.dev/coder/internal/config"
11+
)
12+
13+
type logoutCmd struct {
14+
15+
}
16+
17+
func (cmd logoutCmd) Spec() cli.CommandSpec {
18+
return cli.CommandSpec{
19+
Name: "logout",
20+
Desc: "remote local authentication credentials (if any)",
21+
}
22+
}
23+
24+
func (cmd logoutCmd) Run(_ *pflag.FlagSet) {
25+
err := config.Session.Delete()
26+
if err != nil {
27+
if os.IsNotExist(err) {
28+
flog.Info("no active session")
29+
return
30+
}
31+
flog.Fatal("delete session: %v", err)
32+
}
33+
flog.Success("logged out")
34+
}
35+

cmd/coder/main.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package main
2+
3+
import (
4+
"github.com/spf13/pflag"
5+
"go.coder.com/cli"
6+
)
7+
8+
type rootCmd struct {
9+
}
10+
11+
func (r *rootCmd) Run(fl *pflag.FlagSet) {
12+
fl.Usage()
13+
}
14+
15+
func (r *rootCmd) Spec() cli.CommandSpec {
16+
return cli.CommandSpec{
17+
Name: "coder",
18+
Usage: "[subcommand] [flags]",
19+
Desc: "coder provides a CLI for working with an existing Coder Enterprise installation.",
20+
}
21+
}
22+
23+
func (r *rootCmd) Subcommands() []cli.Command {
24+
return []cli.Command{
25+
loginCmd{},
26+
logoutCmd{},
27+
syncCmd{},
28+
}
29+
}
30+
31+
func main() {
32+
cli.RunRoot(&rootCmd{})
33+
}

cmd/coder/sync.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"strings"
6+
7+
"github.com/spf13/pflag"
8+
"go.coder.com/cli"
9+
"go.coder.com/flog"
10+
11+
"cdr.dev/coder/internal/client"
12+
"cdr.dev/coder/internal/sync"
13+
)
14+
15+
type syncCmd struct {
16+
}
17+
18+
func (cmd syncCmd) Spec() cli.CommandSpec {
19+
return cli.CommandSpec{
20+
Name: "sync",
21+
Usage: "[local directory] [<env name>:<remote directory>]",
22+
Desc: "establish a one way directory sync to a remote environment",
23+
}
24+
}
25+
26+
// userOrgs gets a list of orgs the user is apart of.
27+
func userOrgs(user *client.User, orgs []client.Org) []client.Org {
28+
var uo []client.Org
29+
outer:
30+
for _, org := range orgs {
31+
for _, member := range org.Members {
32+
if member.ID != user.ID {
33+
continue
34+
}
35+
uo = append(uo, org)
36+
continue outer
37+
}
38+
}
39+
return uo
40+
}
41+
42+
func (cmd syncCmd) findEnv(client *client.Client, name string) client.Environment {
43+
me, err := client.Me()
44+
if err != nil {
45+
flog.Fatal("get self: %+v", err)
46+
}
47+
48+
orgs, err := client.Orgs()
49+
if err != nil {
50+
flog.Fatal("get orgs: %+v", err)
51+
}
52+
53+
orgs = userOrgs(me, orgs)
54+
55+
var found []string
56+
57+
for _, org := range orgs {
58+
envs, err := client.Envs(me, org)
59+
if err != nil {
60+
flog.Fatal("get envs for %v: %+v", org.Name, err)
61+
}
62+
for _, env := range envs {
63+
found = append(found, env.Name)
64+
if env.Name != name {
65+
continue
66+
}
67+
return env
68+
}
69+
}
70+
flog.Info("found %q", found)
71+
flog.Fatal("environment %q not found", name)
72+
panic("unreachable")
73+
}
74+
75+
//noinspection GoImportUsedAsName
76+
func (cmd syncCmd) Run(fl *pflag.FlagSet) {
77+
var (
78+
local = fl.Arg(0)
79+
remote = fl.Arg(1)
80+
)
81+
if local == "" || remote == "" {
82+
exitUsage(fl)
83+
}
84+
85+
client := requireAuth()
86+
87+
info, err := os.Stat(local)
88+
if err != nil {
89+
flog.Fatal("%v", err)
90+
}
91+
if !info.IsDir() {
92+
flog.Fatal("%s must be a directory", local)
93+
}
94+
95+
remoteTokens := strings.SplitN(remote, ":", 2)
96+
if len(remoteTokens) != 2 {
97+
flog.Fatal("remote misformmated")
98+
}
99+
var (
100+
envName = remoteTokens[0]
101+
remotePAth = remoteTokens[1]
102+
)
103+
104+
env := cmd.findEnv(client, envName)
105+
_ = remotePAth
106+
107+
s := sync.Sync{
108+
Client: client,
109+
Environment: env,
110+
}
111+
err = s.Run()
112+
if err != nil {
113+
flog.Fatal("sync: %v", err)
114+
}
115+
}

go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module cdr.dev/coder
2+
3+
go 1.14
4+
5+
require (
6+
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
7+
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
8+
github.com/spf13/pflag v1.0.5
9+
go.coder.com/cli v0.4.0
10+
go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512
11+
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect
12+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
13+
nhooyr.io/websocket v1.8.5
14+
)

go.sum

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
2+
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
3+
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
4+
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
5+
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
6+
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
7+
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
8+
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
9+
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU=
10+
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0=
11+
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
12+
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
13+
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
14+
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
15+
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
16+
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
17+
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
18+
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
19+
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
20+
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
21+
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
22+
go.coder.com/cli v0.4.0 h1:PruDGwm/CPFndyK/eMowZG3vzg5CgohRWeXWCTr3zi8=
23+
go.coder.com/cli v0.4.0/go.mod h1:hRTOURCR3LJF1FRW9arecgrzX+AHG7mfYMwThPIgq+w=
24+
go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512 h1:DjCS6dRQh+1PlfiBmnabxfdrzenb0tAwJqFxDEH/s9g=
25+
go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512/go.mod h1:83JsYgXYv0EOaXjIMnaZ1Fl6ddNB3fJnDZ/8845mUJ8=
26+
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
27+
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
28+
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
29+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
30+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
31+
nhooyr.io/websocket v1.8.5 h1:DCqbsbyRh43Ky0pWkdbWXF6z6MS2W8LqJ4ym3F+fw3I=
32+
nhooyr.io/websocket v1.8.5/go.mod h1:szdAKb/TINbpD/bAZy4Ydj5xgVo2BOLNPIi/mcAOGrU=

0 commit comments

Comments
 (0)