From 996a3cd0eb41d50b242fa3cbff2657d091f071aa Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 5 Apr 2021 10:29:22 -0400 Subject: [PATCH 001/128] Fix `coder login` on Windows (#310) * switch to scanner for token * fix spacing * ignore return value from scanner.Scan() --- internal/cmd/login.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/cmd/login.go b/internal/cmd/login.go index 27f4584c..a23cabfd 100644 --- a/internal/cmd/login.go +++ b/internal/cmd/login.go @@ -4,7 +4,6 @@ import ( "bufio" "context" "fmt" - "io" "net/url" "strings" @@ -73,7 +72,15 @@ func login(cmd *cobra.Command, envURL *url.URL) error { fmt.Printf("Your browser has been opened to visit:\n\n\t%s\n\n", authURL.String()) } - token := readLine("Paste token here: ", cmd.InOrStdin()) + fmt.Print("Paste token here: ") + var token string + scanner := bufio.NewScanner(cmd.InOrStdin()) + _ = scanner.Scan() + token = scanner.Text() + if err := scanner.Err(); err != nil { + return xerrors.Errorf("reading standard input: %w", err) + } + if err := pingAPI(cmd.Context(), envURL, token); err != nil { return xerrors.Errorf("ping API with credentials: %w", err) } @@ -84,13 +91,6 @@ func login(cmd *cobra.Command, envURL *url.URL) error { return nil } -func readLine(prompt string, r io.Reader) string { - reader := bufio.NewReader(r) - fmt.Print(prompt) - text, _ := reader.ReadString('\n') - return strings.TrimSuffix(text, "\n") -} - // pingAPI creates a client from the given url/token and try to exec an api call. // Not using the SDK as we want to verify the url/token pair before storing the config files. func pingAPI(ctx context.Context, envURL *url.URL, token string) error { From 739c8e4877e9d8e5165fa72c64e6d111cb4511fd Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 7 Apr 2021 10:59:26 -0500 Subject: [PATCH 002/128] Add coder agent start (#311) * Add coder agent start * Recrease go.sum Co-authored-by: Dean Sheather --- go.mod | 8 +- go.sum | 111 +++++++++++-- internal/cmd/agent.go | 270 +++++++++++++++++++++++++++++++ internal/cmd/cmd.go | 1 + internal/cmd/ssh.go | 4 +- internal/x/xterminal/terminal.go | 6 +- internal/x/xwebrtc/channel.go | 56 +++++++ internal/x/xwebrtc/conn.go | 19 +++ internal/x/xwebrtc/doc.go | 2 + pkg/proto/doc.go | 2 + pkg/proto/message.go | 61 +++++++ 11 files changed, 522 insertions(+), 18 deletions(-) create mode 100644 internal/cmd/agent.go create mode 100644 internal/x/xwebrtc/channel.go create mode 100644 internal/x/xwebrtc/conn.go create mode 100644 internal/x/xwebrtc/doc.go create mode 100644 pkg/proto/doc.go create mode 100644 pkg/proto/message.go diff --git a/go.mod b/go.mod index 295f0066..8014ce0e 100644 --- a/go.mod +++ b/go.mod @@ -9,17 +9,17 @@ require ( github.com/fatih/color v1.10.0 github.com/google/go-cmp v0.5.5 github.com/gorilla/websocket v1.4.2 + github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f github.com/klauspost/compress v1.10.8 // indirect github.com/manifoldco/promptui v0.8.0 + github.com/pion/webrtc/v3 v3.0.20 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.1.3 - github.com/stretchr/testify v1.6.1 // indirect - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 - golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 - golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 + golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005 + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 nhooyr.io/websocket v1.8.6 diff --git a/go.sum b/go.sum index 4cdbb21a..a9c11e0d 100644 --- a/go.sum +++ b/go.sum @@ -81,6 +81,7 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -120,12 +121,19 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= @@ -135,6 +143,8 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -168,6 +178,9 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 h1:Y4V+SFe7d3iH+9pJCoeWIOS5/xBJIFsltS7E+KJSsJY= +github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -230,13 +243,60 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0= +github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= +github.com/pion/dtls/v2 v2.0.8 h1:reGe8rNIMfO/UAeFLqO61tl64t154Qfkr4U3Gzu1tsg= +github.com/pion/dtls/v2 v2.0.8/go.mod h1:QuDII+8FVvk9Dp5t5vYIMTo7hh7uBkra+8QIm7QGm10= +github.com/pion/ice/v2 v2.0.16 h1:K6bzD8ef9vMKbGMTHaUweHXEyuNGnvr2zdqKoLKZPn0= +github.com/pion/ice/v2 v2.0.16/go.mod h1:SJNJzC27gDZoOW0UoxIoC8Hf2PDxG28hQyNdSexDu38= +github.com/pion/interceptor v0.0.12 h1:eC1iVneBIAQJEfaNAfDqAncJWhMDAnaXPRCJsltdokE= +github.com/pion/interceptor v0.0.12/go.mod h1:qzeuWuD/ZXvPqOnxNcnhWfkCZ2e1kwwslicyyPnhoK4= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw= +github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo= +github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= +github.com/pion/rtp v1.6.2 h1:iGBerLX6JiDjB9NXuaPzHyxHFG9JsIEdgwTC0lp5n/U= +github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= +github.com/pion/sctp v1.7.12 h1:GsatLufywVruXbZZT1CKg+Jr8ZTkwiPnmUC/oO9+uuY= +github.com/pion/sctp v1.7.12/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= +github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8= +github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= +github.com/pion/srtp/v2 v2.0.2 h1:664iGzVmaY7KYS5M0gleY0DscRo9ReDfTxQrq4UgGoU= +github.com/pion/srtp/v2 v2.0.2/go.mod h1:VEyLv4CuxrwGY8cxM+Ng3bmVy8ckz/1t6A0q/msKOw0= +github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= +github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= +github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE= +github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= +github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= +github.com/pion/transport v0.12.3 h1:vdBfvfU/0Wq8kd2yhUMSDB/x+O4Z9MYVl2fJ5BT4JZw= +github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A= +github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= +github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= +github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI= +github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= +github.com/pion/webrtc/v3 v3.0.20 h1:Jj0sk45MqQdkR24E1wbFRmOzb1Lv258ot9zd2fYB/Pw= +github.com/pion/webrtc/v3 v3.0.20/go.mod h1:0eJnCpQrUMpRnvyonw4ZiWClToerpixrZ2KcoTxvX9M= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -257,6 +317,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -281,8 +342,10 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= @@ -309,8 +372,9 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -331,6 +395,7 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -344,8 +409,14 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 h1:b0LrWgu8+q7z4J+0Y3Umo5q1dL7NXBkKBWkaVkAq17E= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -359,6 +430,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -373,17 +445,27 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 h1:5jaG59Zhd+8ZXe8C+lgiAGqkOaZBruqrWclLkgAww34= -golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005 h1:pDMpM2zh2MT0kHy037cKlSby2nEhD50SYqwQk76Nm40= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= @@ -439,18 +521,29 @@ google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go new file mode 100644 index 00000000..e7cfebde --- /dev/null +++ b/internal/cmd/agent.go @@ -0,0 +1,270 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/url" + "os" + "strings" + "time" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/sloghuman" + "github.com/hashicorp/yamux" + "github.com/pion/webrtc/v3" + "github.com/spf13/cobra" + "golang.org/x/xerrors" + "nhooyr.io/websocket" + + "cdr.dev/coder-cli/internal/x/xcobra" + "cdr.dev/coder-cli/internal/x/xwebrtc" + "cdr.dev/coder-cli/pkg/proto" +) + +func agentCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "agent", + Short: "Run the workspace agent", + Long: "Connect to Coder and start running a p2p agent", + Hidden: true, + } + + cmd.AddCommand( + startCmd(), + ) + return cmd +} + +func startCmd() *cobra.Command { + var ( + token string + ) + cmd := &cobra.Command{ + Use: "start [coderURL] --token=[token]", + Args: xcobra.ExactArgs(1), + Short: "starts the coder agent", + Long: "starts the coder agent", + Example: `# start the agent and connect with a Coder agent token + +coder agent start https://my-coder.com --token xxxx-xxxx + +# start the agent and use CODER_AGENT_TOKEN env var for auth token + +coder agent start https://my-coder.com +`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + log := slog.Make(sloghuman.Sink(cmd.OutOrStdout())) + + // Pull the URL from the args and do some sanity check. + rawURL := args[0] + if rawURL == "" || !strings.HasPrefix(rawURL, "http") { + return xerrors.Errorf("invalid URL") + } + u, err := url.Parse(rawURL) + if err != nil { + return xerrors.Errorf("parse url: %w", err) + } + // Remove the trailing '/' if any. + u.Path = "/api/private/envagent/listen" + + if token == "" { + var ok bool + token, ok = os.LookupEnv("CODER_AGENT_TOKEN") + if !ok { + return xerrors.New("must pass --token or set the CODER_AGENT_TOKEN env variable") + } + } + + q := u.Query() + q.Set("service_token", token) + u.RawQuery = q.Encode() + + ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) + defer cancelFunc() + log.Info(ctx, "connecting to broker", slog.F("url", u.String())) + conn, res, err := websocket.Dial(ctx, u.String(), nil) + if err != nil { + return fmt.Errorf("dial: %w", err) + } + _ = res.Body.Close() + nc := websocket.NetConn(context.Background(), conn, websocket.MessageBinary) + session, err := yamux.Server(nc, nil) + if err != nil { + return fmt.Errorf("open: %w", err) + } + log.Info(ctx, "connected to broker. awaiting connection requests") + for { + st, err := session.AcceptStream() + if err != nil { + return fmt.Errorf("accept stream: %w", err) + } + stream := &stream{ + logger: log.Named(fmt.Sprintf("stream %d", st.StreamID())), + stream: st, + } + go stream.listen() + } + }, + } + + cmd.Flags().StringVar(&token, "token", "", "coder agent token") + return cmd +} + +type stream struct { + stream *yamux.Stream + logger slog.Logger + + rtc *webrtc.PeerConnection +} + +// writes an error and closes. +func (s *stream) fatal(err error) { + _ = s.write(proto.Message{ + Error: err.Error(), + }) + s.logger.Error(context.Background(), err.Error(), slog.Error(err)) + _ = s.stream.Close() +} + +func (s *stream) listen() { + decoder := json.NewDecoder(s.stream) + for { + var msg proto.Message + err := decoder.Decode(&msg) + if err == io.EOF { + break + } + if err != nil { + s.fatal(err) + return + } + s.processMessage(msg) + } +} + +func (s *stream) write(msg proto.Message) error { + d, err := json.Marshal(&msg) + if err != nil { + return err + } + _, err = s.stream.Write(d) + if err != nil { + return err + } + return nil +} + +func (s *stream) processMessage(msg proto.Message) { + s.logger.Debug(context.Background(), "processing message", slog.F("msg", msg)) + + if msg.Error != "" { + s.fatal(xerrors.New(msg.Error)) + return + } + + if msg.Candidate != "" { + if s.rtc == nil { + s.fatal(xerrors.New("rtc connection must be started before candidates are sent")) + return + } + + s.logger.Debug(context.Background(), "accepted ice candidate", slog.F("candidate", msg.Candidate)) + err := proto.AcceptICECandidate(s.rtc, &msg) + if err != nil { + s.fatal(err) + return + } + } + + if msg.Offer != nil { + rtc, err := xwebrtc.NewPeerConnection() + if err != nil { + s.fatal(fmt.Errorf("create connection: %w", err)) + return + } + flushCandidates := proto.ProxyICECandidates(rtc, s.stream) + + err = rtc.SetRemoteDescription(*msg.Offer) + if err != nil { + s.fatal(fmt.Errorf("set remote desc: %w", err)) + return + } + answer, err := rtc.CreateAnswer(nil) + if err != nil { + s.fatal(fmt.Errorf("create answer: %w", err)) + return + } + err = rtc.SetLocalDescription(answer) + if err != nil { + s.fatal(fmt.Errorf("set local desc: %w", err)) + return + } + flushCandidates() + + err = s.write(proto.Message{ + Answer: rtc.LocalDescription(), + }) + if err != nil { + s.fatal(fmt.Errorf("send local desc: %w", err)) + return + } + + rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { + s.logger.Info(context.Background(), "state changed", slog.F("new", pcs)) + }) + rtc.OnDataChannel(s.processDataChannel) + s.rtc = rtc + } +} + +func (s *stream) processDataChannel(channel *webrtc.DataChannel) { + if channel.Protocol() == "ping" { + channel.OnOpen(func() { + rw, err := channel.Detach() + if err != nil { + return + } + d := make([]byte, 64) + _, _ = rw.Read(d) + _, _ = rw.Write(d) + }) + return + } + + prto, port, err := xwebrtc.ParseProxyDataChannel(channel) + if err != nil { + s.fatal(fmt.Errorf("failed to parse proxy data channel: %w", err)) + return + } + if prto != "tcp" { + s.fatal(fmt.Errorf("client provided unsupported protocol: %s", prto)) + return + } + + conn, err := net.Dial(prto, fmt.Sprintf("localhost:%d", port)) + if err != nil { + s.fatal(fmt.Errorf("failed to dial client port: %d", port)) + return + } + + channel.OnOpen(func() { + s.logger.Debug(context.Background(), "proxying data channel to local port", slog.F("port", port)) + rw, err := channel.Detach() + if err != nil { + _ = channel.Close() + s.logger.Error(context.Background(), "detach client data channel", slog.Error(err)) + return + } + go func() { + _, _ = io.Copy(rw, conn) + }() + go func() { + _, _ = io.Copy(conn, rw) + }() + }) +} diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 888fd84c..a64fa0a9 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -37,6 +37,7 @@ func Make() *cobra.Command { imgsCmd(), providersCmd(), genDocsCmd(app), + agentCmd(), ) app.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show verbose output") return app diff --git a/internal/cmd/ssh.go b/internal/cmd/ssh.go index 18131717..f062fa11 100644 --- a/internal/cmd/ssh.go +++ b/internal/cmd/ssh.go @@ -9,7 +9,7 @@ import ( "path/filepath" "github.com/spf13/cobra" - "golang.org/x/crypto/ssh/terminal" + "golang.org/x/term" "golang.org/x/xerrors" "cdr.dev/coder-cli/coder-sdk" @@ -17,7 +17,7 @@ import ( ) var ( - showInteractiveOutput = terminal.IsTerminal(int(os.Stdout.Fd())) + showInteractiveOutput = term.IsTerminal(int(os.Stdout.Fd())) ) func sshCmd() *cobra.Command { diff --git a/internal/x/xterminal/terminal.go b/internal/x/xterminal/terminal.go index 2d420bf1..49ae7cba 100644 --- a/internal/x/xterminal/terminal.go +++ b/internal/x/xterminal/terminal.go @@ -3,12 +3,12 @@ package xterminal import ( - "golang.org/x/crypto/ssh/terminal" + "golang.org/x/term" ) // State differs per-platform. type State struct { - s *terminal.State + s *term.State } // MakeOutputRaw does nothing on non-Windows platforms. @@ -20,5 +20,5 @@ func Restore(fd uintptr, state *State) error { return nil } - return terminal.Restore(int(fd), state.s) + return term.Restore(int(fd), state.s) } diff --git a/internal/x/xwebrtc/channel.go b/internal/x/xwebrtc/channel.go new file mode 100644 index 00000000..08442d9e --- /dev/null +++ b/internal/x/xwebrtc/channel.go @@ -0,0 +1,56 @@ +package xwebrtc + +import ( + "context" + "errors" + "fmt" + "net" + "strconv" + "time" + + "github.com/pion/webrtc/v3" +) + +// WaitForDataChannelOpen waits for the data channel to have the open state. +// By default, it waits 15 seconds. +func WaitForDataChannelOpen(ctx context.Context, channel *webrtc.DataChannel) error { + if channel.ReadyState() == webrtc.DataChannelStateOpen { + return nil + } + ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) + defer cancelFunc() + channel.OnOpen(func() { + cancelFunc() + }) + <-ctx.Done() + if ctx.Err() == context.DeadlineExceeded { + return ctx.Err() + } + return nil +} + +// NewProxyDataChannel creates a new data channel for proxying. +func NewProxyDataChannel(conn *webrtc.PeerConnection, name, protocol string, port uint16) (*webrtc.DataChannel, error) { + proto := fmt.Sprintf("%s:%d", protocol, port) + ordered := true + return conn.CreateDataChannel(name, &webrtc.DataChannelInit{ + Protocol: &proto, + Ordered: &ordered, + }) +} + +// ParseProxyDataChannel parses a data channel to get the protocol and port. +func ParseProxyDataChannel(channel *webrtc.DataChannel) (string, uint16, error) { + if channel.Protocol() == "" { + return "", 0, errors.New("data channel is not a proxy") + } + host, port, err := net.SplitHostPort(channel.Protocol()) + if err != nil { + return "", 0, fmt.Errorf("split protocol: %w", err) + } + p, err := strconv.ParseInt(port, 10, 16) + if err != nil { + return "", 0, fmt.Errorf("parse port: %w", err) + } + return host, uint16(p), nil +} diff --git a/internal/x/xwebrtc/conn.go b/internal/x/xwebrtc/conn.go new file mode 100644 index 00000000..20178f2e --- /dev/null +++ b/internal/x/xwebrtc/conn.go @@ -0,0 +1,19 @@ +package xwebrtc + +import "github.com/pion/webrtc/v3" + +// NewPeerConnection creates a new peer connection. +// It uses the Google stun server by default. +func NewPeerConnection() (*webrtc.PeerConnection, error) { + se := webrtc.SettingEngine{} + se.DetachDataChannels() + api := webrtc.NewAPI(webrtc.WithSettingEngine(se)) + + return api.NewPeerConnection(webrtc.Configuration{ + ICEServers: []webrtc.ICEServer{ + { + URLs: []string{"stun:stun.l.google.com:19302"}, + }, + }, + }) +} diff --git a/internal/x/xwebrtc/doc.go b/internal/x/xwebrtc/doc.go new file mode 100644 index 00000000..ba5f4181 --- /dev/null +++ b/internal/x/xwebrtc/doc.go @@ -0,0 +1,2 @@ +// Package xwebrtc contains utilities for working with webrtc connections. +package xwebrtc diff --git a/pkg/proto/doc.go b/pkg/proto/doc.go new file mode 100644 index 00000000..65f4d1e5 --- /dev/null +++ b/pkg/proto/doc.go @@ -0,0 +1,2 @@ +// Package proto contains shared messages for webrtc handshakes. +package proto diff --git a/pkg/proto/message.go b/pkg/proto/message.go new file mode 100644 index 00000000..b6d7876d --- /dev/null +++ b/pkg/proto/message.go @@ -0,0 +1,61 @@ +package proto + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/pion/webrtc/v3" +) + +// Message is a common format for agent and client to use in handshake. +type Message struct { + Error string `json:"error"` + Candidate string `json:"candidate"` + Offer *webrtc.SessionDescription `json:"offer"` + Answer *webrtc.SessionDescription `json:"answer"` +} + +// WriteError responds with an error and status code. +func WriteError(w http.ResponseWriter, status int, err error) { + w.WriteHeader(status) + _, _ = w.Write([]byte(err.Error())) +} + +// ProxyICECandidates sends all ICE candidates using the message protocol +// to the writer provided. +func ProxyICECandidates(conn *webrtc.PeerConnection, w io.Writer) func() { + queue := make([]*webrtc.ICECandidate, 0) + flushed := false + write := func(i *webrtc.ICECandidate) { + b, _ := json.Marshal(&Message{ + Candidate: i.ToJSON().Candidate, + }) + _, _ = w.Write(b) + } + + conn.OnICECandidate(func(i *webrtc.ICECandidate) { + if i == nil { + return + } + if !flushed { + queue = append(queue, i) + return + } + + write(i) + }) + return func() { + for _, i := range queue { + write(i) + } + flushed = true + } +} + +// AcceptICECandidate adds the candidate to the connection. +func AcceptICECandidate(conn *webrtc.PeerConnection, m *Message) error { + return conn.AddICECandidate(webrtc.ICECandidateInit{ + Candidate: m.Candidate, + }) +} From c1a999446392cdaeb26d8806b9ced43ca74cb444 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 8 Apr 2021 14:51:41 -0500 Subject: [PATCH 003/128] Add coder tunnel command (#313) * Add coder tunnel command * Fix auth for p2p --- docs/coder_config-ssh.md | 1 + internal/cmd/agent.go | 48 +++++-- internal/cmd/cmd.go | 1 + internal/cmd/configssh.go | 27 +++- internal/cmd/tunnel.go | 273 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 330 insertions(+), 20 deletions(-) create mode 100644 internal/cmd/tunnel.go diff --git a/docs/coder_config-ssh.md b/docs/coder_config-ssh.md index 8ac849cf..311387aa 100644 --- a/docs/coder_config-ssh.md +++ b/docs/coder_config-ssh.md @@ -15,6 +15,7 @@ coder config-ssh [flags] ``` --filepath string override the default path of your ssh config file (default "~/.ssh/config") -h, --help help for config-ssh + --p2p (experimental) uses coder tunnel to proxy ssh connection --remove remove the auto-generated Coder ssh config ``` diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go index e7cfebde..dbe7d80a 100644 --- a/internal/cmd/agent.go +++ b/internal/cmd/agent.go @@ -19,7 +19,6 @@ import ( "golang.org/x/xerrors" "nhooyr.io/websocket" - "cdr.dev/coder-cli/internal/x/xcobra" "cdr.dev/coder-cli/internal/x/xwebrtc" "cdr.dev/coder-cli/pkg/proto" ) @@ -40,31 +39,42 @@ func agentCmd() *cobra.Command { func startCmd() *cobra.Command { var ( - token string + token string + coderURL string ) cmd := &cobra.Command{ - Use: "start [coderURL] --token=[token]", - Args: xcobra.ExactArgs(1), + Use: "start --coder-url=[coder_url] --token=[token]", Short: "starts the coder agent", Long: "starts the coder agent", - Example: `# start the agent and connect with a Coder agent token + Example: `# start the agent and use CODER_URL and CODER_AGENT_TOKEN env vars -coder agent start https://my-coder.com --token xxxx-xxxx +coder agent start -# start the agent and use CODER_AGENT_TOKEN env var for auth token +# start the agent and connect with a specified url and agent token -coder agent start https://my-coder.com +coder agent start --coder-url https://my-coder.com --token xxxx-xxxx `, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() log := slog.Make(sloghuman.Sink(cmd.OutOrStdout())) - // Pull the URL from the args and do some sanity check. - rawURL := args[0] - if rawURL == "" || !strings.HasPrefix(rawURL, "http") { + if coderURL == "" { + var ok bool + token, ok = os.LookupEnv("CODER_URL") + if !ok { + client, err := newClient(ctx) + if err != nil { + return xerrors.New("must login, pass --coder-url flag, or set the CODER_URL env variable") + } + burl := client.BaseURL() + coderURL = burl.String() + } + } + + if !strings.HasPrefix(coderURL, "http") { return xerrors.Errorf("invalid URL") } - u, err := url.Parse(rawURL) + u, err := url.Parse(coderURL) if err != nil { return xerrors.Errorf("parse url: %w", err) } @@ -79,6 +89,14 @@ coder agent start https://my-coder.com } } + if token == "" { + var ok bool + token, ok = os.LookupEnv("CODER_AGENT_TOKEN") + if !ok { + return xerrors.New("must pass --token or set the CODER_AGENT_TOKEN env variable") + } + } + q := u.Query() q.Set("service_token", token) u.RawQuery = q.Encode() @@ -86,11 +104,11 @@ coder agent start https://my-coder.com ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) defer cancelFunc() log.Info(ctx, "connecting to broker", slog.F("url", u.String())) - conn, res, err := websocket.Dial(ctx, u.String(), nil) + // nolint: bodyclose + conn, _, err := websocket.Dial(ctx, u.String(), nil) if err != nil { return fmt.Errorf("dial: %w", err) } - _ = res.Body.Close() nc := websocket.NetConn(context.Background(), conn, websocket.MessageBinary) session, err := yamux.Server(nc, nil) if err != nil { @@ -112,6 +130,8 @@ coder agent start https://my-coder.com } cmd.Flags().StringVar(&token, "token", "", "coder agent token") + cmd.Flags().StringVar(&coderURL, "coder-url", "", "coder access url") + return cmd } diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index a64fa0a9..be7b8f9e 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -38,6 +38,7 @@ func Make() *cobra.Command { providersCmd(), genDocsCmd(app), agentCmd(), + tunnelCmd(), ) app.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show verbose output") return app diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index 9bfb0cd4..120e9a86 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -35,21 +35,23 @@ func configSSHCmd() *cobra.Command { var ( configpath string remove = false + p2p = false ) cmd := &cobra.Command{ Use: "config-ssh", Short: "Configure SSH to access Coder environments", Long: "Inject the proper OpenSSH configuration into your local SSH config file.", - RunE: configSSH(&configpath, &remove), + RunE: configSSH(&configpath, &remove, &p2p), } cmd.Flags().StringVar(&configpath, "filepath", filepath.Join("~", ".ssh", "config"), "override the default path of your ssh config file") cmd.Flags().BoolVar(&remove, "remove", false, "remove the auto-generated Coder ssh config") + cmd.Flags().BoolVar(&p2p, "p2p", false, "(experimental) uses coder tunnel to proxy ssh connection") return cmd } -func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []string) error { +func configSSH(configpath *string, remove *bool, p2p *bool) func(cmd *cobra.Command, _ []string) error { return func(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() usr, err := user.Current() @@ -113,7 +115,7 @@ func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []st return xerrors.New("SSH is disabled or not available for any environments in your Coder deployment.") } - newConfig := makeNewConfigs(user.Username, envsWithProviders, privateKeyFilepath) + newConfig := makeNewConfigs(user.Username, envsWithProviders, privateKeyFilepath, *p2p) err = os.MkdirAll(filepath.Dir(*configpath), os.ModePerm) if err != nil { @@ -174,7 +176,7 @@ func writeSSHKey(ctx context.Context, client coder.Client, privateKeyPath string return ioutil.WriteFile(privateKeyPath, []byte(key.PrivateKey), 0600) } -func makeNewConfigs(userName string, envs []coderutil.EnvWithWorkspaceProvider, privateKeyFilepath string) string { +func makeNewConfigs(userName string, envs []coderutil.EnvWithWorkspaceProvider, privateKeyFilepath string, p2p bool) string { newConfig := fmt.Sprintf("\n%s\n%s\n\n", sshStartToken, sshStartMessage) sort.Slice(envs, func(i, j int) bool { return envs[i].Env.Name < envs[j].Env.Name }) @@ -192,14 +194,27 @@ func makeNewConfigs(userName string, envs []coderutil.EnvWithWorkspaceProvider, clog.LogWarn("invalid access url", clog.Causef("malformed url: %q", env.WorkspaceProvider.EnvproxyAccessURL)) continue } - newConfig += makeSSHConfig(u.Host, userName, env.Env.Name, privateKeyFilepath) + newConfig += makeSSHConfig(u.Host, userName, env.Env.Name, privateKeyFilepath, p2p) } newConfig += fmt.Sprintf("\n%s\n", sshEndToken) return newConfig } -func makeSSHConfig(host, userName, envName, privateKeyFilepath string) string { +func makeSSHConfig(host, userName, envName, privateKeyFilepath string, p2p bool) string { + if p2p { + return fmt.Sprintf( + `Host coder.%s + HostName localhost + ProxyCommand coder tunnel %s 22 stdio + StrictHostKeyChecking no + ConnectTimeout=0 + IdentityFile="%s" + ServerAliveInterval 60 + ServerAliveCountMax 3 +`, envName, envName, privateKeyFilepath) + } + return fmt.Sprintf( `Host coder.%s HostName %s diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go new file mode 100644 index 00000000..fbfd0a1c --- /dev/null +++ b/internal/cmd/tunnel.go @@ -0,0 +1,273 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "os" + "strconv" + "time" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/sloghuman" + "github.com/pion/webrtc/v3" + "github.com/spf13/cobra" + "golang.org/x/xerrors" + "nhooyr.io/websocket" + + "cdr.dev/coder-cli/internal/x/xcobra" + "cdr.dev/coder-cli/internal/x/xwebrtc" + "cdr.dev/coder-cli/pkg/proto" +) + +func tunnelCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "tunnel [workspace_name] [workspace_port] [localhost_port]", + Args: xcobra.ExactArgs(3), + Short: "proxies a port on the workspace to localhost", + Long: "proxies a port on the workspace to localhost", + Example: `# run a tcp tunnel from the workspace on port 3000 to localhost:3000 + +coder tunnel my-dev 3000 3000 +`, + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + log := slog.Make(sloghuman.Sink(os.Stderr)) + + remotePort, err := strconv.ParseUint(args[1], 10, 16) + if err != nil { + log.Fatal(ctx, "parse remote port", slog.Error(err)) + } + + var localPort uint64 + if args[2] != "stdio" { + localPort, err = strconv.ParseUint(args[2], 10, 16) + if err != nil { + log.Fatal(ctx, "parse local port", slog.Error(err)) + } + } + + sdk, err := newClient(ctx) + if err != nil { + return err + } + baseURL := sdk.BaseURL() + + envs, err := sdk.Environments(ctx) + if err != nil { + return err + } + + var envID string + for _, env := range envs { + if env.Name == args[0] { + envID = env.ID + break + } + } + if envID == "" { + return xerrors.Errorf("No workspace found by name '%s'", args[0]) + } + + c := &client{ + id: envID, + stdio: args[2] == "stdio", + localPort: uint16(localPort), + remotePort: uint16(remotePort), + ctx: context.Background(), + logger: log, + brokerAddr: baseURL.String(), + token: sdk.Token(), + } + + err = c.start() + if err != nil { + log.Fatal(ctx, err.Error()) + } + + return nil + }, + } + + return cmd +} + +type client struct { + ctx context.Context + brokerAddr string + token string + logger slog.Logger + id string + remotePort uint16 + localPort uint16 + stdio bool +} + +func (c *client) start() error { + url := fmt.Sprintf("%s%s%s%s%s", c.brokerAddr, "/api/private/envagent/", c.id, "/connect?session_token=", c.token) + c.logger.Info(c.ctx, "connecting to broker", slog.F("url", url)) + + conn, _, err := websocket.Dial(c.ctx, url, nil) + if err != nil { + return fmt.Errorf("dial: %w", err) + } + nconn := websocket.NetConn(context.Background(), conn, websocket.MessageBinary) + + rtc, err := xwebrtc.NewPeerConnection() + if err != nil { + return fmt.Errorf("create connection: %w", err) + } + + rtc.OnNegotiationNeeded(func() { + c.logger.Debug(context.Background(), "negotiation needed...") + }) + + rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { + c.logger.Info(context.Background(), "connection state changed", slog.F("state", pcs)) + }) + + channel, err := xwebrtc.NewProxyDataChannel(rtc, "forwarder", "tcp", c.remotePort) + if err != nil { + return fmt.Errorf("create data channel: %w", err) + } + flushCandidates := proto.ProxyICECandidates(rtc, nconn) + + localDesc, err := rtc.CreateOffer(&webrtc.OfferOptions{}) + if err != nil { + return fmt.Errorf("create offer: %w", err) + } + + err = rtc.SetLocalDescription(localDesc) + if err != nil { + return fmt.Errorf("set local desc: %w", err) + } + flushCandidates() + + c.logger.Debug(context.Background(), "writing offer") + b, _ := json.Marshal(&proto.Message{ + Offer: &localDesc, + }) + _, err = nconn.Write(b) + if err != nil { + return fmt.Errorf("write offer: %w", err) + } + + go func() { + err = xwebrtc.WaitForDataChannelOpen(context.Background(), channel) + if err != nil { + c.logger.Fatal(context.Background(), "waiting for data channel open", slog.Error(err)) + } + _ = conn.Close(websocket.StatusNormalClosure, "rtc connected") + }() + + decoder := json.NewDecoder(nconn) + for { + var msg proto.Message + err = decoder.Decode(&msg) + if err == io.EOF { + break + } + if websocket.CloseStatus(err) == websocket.StatusNormalClosure { + break + } + if err != nil { + return fmt.Errorf("read msg: %w", err) + } + if msg.Candidate != "" { + c.logger.Debug(context.Background(), "accepted ice candidate", slog.F("candidate", msg.Candidate)) + err = proto.AcceptICECandidate(rtc, &msg) + if err != nil { + return fmt.Errorf("accept ice: %w", err) + } + } + if msg.Answer != nil { + c.logger.Debug(context.Background(), "got answer", slog.F("answer", msg.Answer)) + err = rtc.SetRemoteDescription(*msg.Answer) + if err != nil { + return fmt.Errorf("set remote: %w", err) + } + } + } + + // Once we're open... let's test out the ping. + pingProto := "ping" + pingChannel, err := rtc.CreateDataChannel("pinger", &webrtc.DataChannelInit{ + Protocol: &pingProto, + }) + if err != nil { + return fmt.Errorf("create ping channel") + } + pingChannel.OnOpen(func() { + defer func() { + _ = pingChannel.Close() + }() + t1 := time.Now() + rw, _ := pingChannel.Detach() + defer func() { + _ = rw.Close() + }() + _, _ = rw.Write([]byte("hello")) + b := make([]byte, 64) + _, _ = rw.Read(b) + c.logger.Info(c.ctx, "your latency directly to the agent", slog.F("ms", time.Since(t1).Milliseconds())) + }) + + if c.stdio { + // At this point the RTC is connected and data channel is opened... + rw, err := channel.Detach() + if err != nil { + return fmt.Errorf("detach channel: %w", err) + } + go func() { + _, _ = io.Copy(rw, os.Stdin) + }() + _, err = io.Copy(os.Stdout, rw) + if err != nil { + return fmt.Errorf("copy: %w", err) + } + return nil + } + + listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", c.localPort)) + if err != nil { + return fmt.Errorf("listen: %w", err) + } + + for { + conn, err := listener.Accept() + if err != nil { + return fmt.Errorf("accept: %w", err) + } + go func() { + defer func() { + _ = conn.Close() + }() + channel, err := xwebrtc.NewProxyDataChannel(rtc, "forwarder", "tcp", c.remotePort) + if err != nil { + c.logger.Warn(context.Background(), "create data channel for proxying", slog.Error(err)) + return + } + defer func() { + _ = channel.Close() + }() + err = xwebrtc.WaitForDataChannelOpen(context.Background(), channel) + if err != nil { + c.logger.Warn(context.Background(), "wait for data channel open", slog.Error(err)) + return + } + rw, err := channel.Detach() + if err != nil { + c.logger.Warn(context.Background(), "detach channel", slog.Error(err)) + return + } + + go func() { + _, _ = io.Copy(conn, rw) + }() + _, _ = io.Copy(rw, conn) + }() + } +} From 2b0af5ac29fba61df3d374abc244fd35519720bb Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 16 Apr 2021 14:40:11 -0500 Subject: [PATCH 004/128] Add retries to coder agent start cmd (#316) * Add retries to coder agent start cmd * Add more error logs --- agent/doc.go | 2 + agent/server.go | 89 +++++++++++++++++ agent/stream.go | 185 +++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + internal/cmd/agent.go | 219 +++--------------------------------------- 6 files changed, 293 insertions(+), 205 deletions(-) create mode 100644 agent/doc.go create mode 100644 agent/server.go create mode 100644 agent/stream.go diff --git a/agent/doc.go b/agent/doc.go new file mode 100644 index 00000000..46ebd899 --- /dev/null +++ b/agent/doc.go @@ -0,0 +1,2 @@ +// Package agent is for interacting with p2p server and clients +package agent diff --git a/agent/server.go b/agent/server.go new file mode 100644 index 00000000..17bb51f7 --- /dev/null +++ b/agent/server.go @@ -0,0 +1,89 @@ +package agent + +import ( + "context" + "fmt" + "net/url" + "time" + + "cdr.dev/slog" + "github.com/hashicorp/yamux" + "go.coder.com/retry" + "golang.org/x/xerrors" + "nhooyr.io/websocket" +) + +const ( + listenRoute = "/api/private/envagent/listen" +) + +// Server connects to a Coder deployment and listens for p2p connections. +type Server struct { + log slog.Logger + listenURL *url.URL +} + +// ServerArgs are the required arguments to create an agent server. +type ServerArgs struct { + Log slog.Logger + CoderURL *url.URL + Token string +} + +// NewServer creates a new agent server. +func NewServer(args ServerArgs) (*Server, error) { + lURL, err := formatListenURL(args.CoderURL, args.Token) + if err != nil { + return nil, xerrors.Errorf("formatting listen url: %w", err) + } + + return &Server{ + log: args.Log, + listenURL: lURL, + }, nil +} + +// Run will listen and proxy new peer connections on a retry loop. +func (s *Server) Run(ctx context.Context) error { + err := retry.New(time.Second).Context(ctx).Backoff(15 * time.Second).Run(func() error { + ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) + defer cancelFunc() + s.log.Info(ctx, "connecting to coder", slog.F("url", s.listenURL.String())) + conn, _, err := websocket.Dial(ctx, s.listenURL.String(), nil) + if err != nil { + return fmt.Errorf("dial: %w", err) + } + nc := websocket.NetConn(context.Background(), conn, websocket.MessageBinary) + session, err := yamux.Server(nc, nil) + if err != nil { + return fmt.Errorf("open: %w", err) + } + s.log.Info(ctx, "connected to coder. awaiting connection requests") + for { + st, err := session.AcceptStream() + if err != nil { + return fmt.Errorf("accept stream: %w", err) + } + stream := &stream{ + logger: s.log.Named(fmt.Sprintf("stream %d", st.StreamID())), + stream: st, + } + go stream.listen() + } + }) + + return err +} + +func formatListenURL(coderURL *url.URL, token string) (*url.URL, error) { + if coderURL.Scheme != "http" && coderURL.Scheme != "https" { + return nil, xerrors.Errorf("invalid URL scheme") + } + + coderURL.Path = listenRoute + q := coderURL.Query() + q.Set("service_token", token) + coderURL.RawQuery = q.Encode() + + return coderURL, nil +} diff --git a/agent/stream.go b/agent/stream.go new file mode 100644 index 00000000..42a2233f --- /dev/null +++ b/agent/stream.go @@ -0,0 +1,185 @@ +package agent + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + + "cdr.dev/slog" + "github.com/hashicorp/yamux" + "github.com/pion/webrtc/v3" + "golang.org/x/xerrors" + + "cdr.dev/coder-cli/internal/x/xwebrtc" + "cdr.dev/coder-cli/pkg/proto" +) + +type stream struct { + stream *yamux.Stream + logger slog.Logger + + rtc *webrtc.PeerConnection +} + +// writes an error and closes. +func (s *stream) fatal(err error) { + _ = s.write(proto.Message{ + Error: err.Error(), + }) + s.logger.Error(context.Background(), err.Error(), slog.Error(err)) + _ = s.stream.Close() +} + +func (s *stream) listen() { + decoder := json.NewDecoder(s.stream) + for { + var msg proto.Message + err := decoder.Decode(&msg) + if err == io.EOF { + break + } + if err != nil { + s.fatal(err) + return + } + s.processMessage(msg) + } +} + +func (s *stream) write(msg proto.Message) error { + d, err := json.Marshal(&msg) + if err != nil { + return err + } + _, err = s.stream.Write(d) + if err != nil { + return err + } + return nil +} + +func (s *stream) processMessage(msg proto.Message) { + s.logger.Debug(context.Background(), "processing message", slog.F("msg", msg)) + + if msg.Error != "" { + s.fatal(xerrors.New(msg.Error)) + return + } + + if msg.Candidate != "" { + if s.rtc == nil { + s.fatal(xerrors.New("rtc connection must be started before candidates are sent")) + return + } + + s.logger.Debug(context.Background(), "accepted ice candidate", slog.F("candidate", msg.Candidate)) + err := proto.AcceptICECandidate(s.rtc, &msg) + if err != nil { + s.fatal(err) + return + } + } + + if msg.Offer != nil { + rtc, err := xwebrtc.NewPeerConnection() + if err != nil { + s.fatal(fmt.Errorf("create connection: %w", err)) + return + } + flushCandidates := proto.ProxyICECandidates(rtc, s.stream) + + err = rtc.SetRemoteDescription(*msg.Offer) + if err != nil { + s.fatal(fmt.Errorf("set remote desc: %w", err)) + return + } + answer, err := rtc.CreateAnswer(nil) + if err != nil { + s.fatal(fmt.Errorf("create answer: %w", err)) + return + } + err = rtc.SetLocalDescription(answer) + if err != nil { + s.fatal(fmt.Errorf("set local desc: %w", err)) + return + } + flushCandidates() + + err = s.write(proto.Message{ + Answer: rtc.LocalDescription(), + }) + if err != nil { + s.fatal(fmt.Errorf("send local desc: %w", err)) + return + } + + rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { + s.logger.Info(context.Background(), "state changed", slog.F("new", pcs)) + }) + rtc.OnDataChannel(s.processDataChannel) + s.rtc = rtc + } +} + +func (s *stream) processDataChannel(channel *webrtc.DataChannel) { + if channel.Protocol() == "ping" { + channel.OnOpen(func() { + rw, err := channel.Detach() + if err != nil { + return + } + d := make([]byte, 64) + _, err = rw.Read(d) + if err != nil { + s.logger.Error(context.Background(), "read ping", slog.Error(err)) + return + } + _, err = rw.Write(d) + if err != nil { + s.logger.Error(context.Background(), "write ping", slog.Error(err)) + return + } + }) + return + } + + prto, port, err := xwebrtc.ParseProxyDataChannel(channel) + if err != nil { + s.fatal(fmt.Errorf("failed to parse proxy data channel: %w", err)) + return + } + if prto != "tcp" { + s.fatal(fmt.Errorf("client provided unsupported protocol: %s", prto)) + return + } + + conn, err := net.Dial(prto, fmt.Sprintf("localhost:%d", port)) + if err != nil { + s.fatal(fmt.Errorf("failed to dial client port: %d", port)) + return + } + + channel.OnOpen(func() { + s.logger.Debug(context.Background(), "proxying data channel to local port", slog.F("port", port)) + rw, err := channel.Detach() + if err != nil { + _ = channel.Close() + s.logger.Error(context.Background(), "detach client data channel", slog.Error(err)) + return + } + go func() { + _, err = io.Copy(rw, conn) + if err != nil { + s.logger.Error(context.Background(), "copy to conn", slog.Error(err)) + } + }() + go func() { + _, _ = io.Copy(conn, rw) + if err != nil { + s.logger.Error(context.Background(), "copy from conn", slog.Error(err)) + } + }() + }) +} diff --git a/go.mod b/go.mod index 8014ce0e..1a88ad94 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.1.3 + go.coder.com/retry v1.2.0 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 diff --git a/go.sum b/go.sum index a9c11e0d..0ec0d443 100644 --- a/go.sum +++ b/go.sum @@ -358,6 +358,8 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q go.coder.com/cli v0.4.0/go.mod h1:hRTOURCR3LJF1FRW9arecgrzX+AHG7mfYMwThPIgq+w= go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512 h1:DjCS6dRQh+1PlfiBmnabxfdrzenb0tAwJqFxDEH/s9g= go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512/go.mod h1:83JsYgXYv0EOaXjIMnaZ1Fl6ddNB3fJnDZ/8845mUJ8= +go.coder.com/retry v1.2.0 h1:ODdUPu9cb9pcbeAM5j2YqJHUgfFbN60vmhtlWIKZGLo= +go.coder.com/retry v1.2.0/go.mod h1:ihkJszQk8F+yaFL2pcIku9MzbYo+U8vka4IsvQSXVfE= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go index dbe7d80a..84995bbb 100644 --- a/internal/cmd/agent.go +++ b/internal/cmd/agent.go @@ -2,25 +2,15 @@ package cmd import ( "context" - "encoding/json" - "fmt" - "io" - "net" "net/url" "os" - "strings" - "time" "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" - "github.com/hashicorp/yamux" - "github.com/pion/webrtc/v3" "github.com/spf13/cobra" "golang.org/x/xerrors" - "nhooyr.io/websocket" - "cdr.dev/coder-cli/internal/x/xwebrtc" - "cdr.dev/coder-cli/pkg/proto" + "cdr.dev/coder-cli/agent" ) func agentCmd() *cobra.Command { @@ -60,7 +50,7 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx if coderURL == "" { var ok bool - token, ok = os.LookupEnv("CODER_URL") + coderURL, ok = os.LookupEnv("CODER_URL") if !ok { client, err := newClient(ctx) if err != nil { @@ -71,15 +61,10 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx } } - if !strings.HasPrefix(coderURL, "http") { - return xerrors.Errorf("invalid URL") - } u, err := url.Parse(coderURL) if err != nil { return xerrors.Errorf("parse url: %w", err) } - // Remove the trailing '/' if any. - u.Path = "/api/private/envagent/listen" if token == "" { var ok bool @@ -89,43 +74,21 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx } } - if token == "" { - var ok bool - token, ok = os.LookupEnv("CODER_AGENT_TOKEN") - if !ok { - return xerrors.New("must pass --token or set the CODER_AGENT_TOKEN env variable") - } - } - - q := u.Query() - q.Set("service_token", token) - u.RawQuery = q.Encode() - - ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) - defer cancelFunc() - log.Info(ctx, "connecting to broker", slog.F("url", u.String())) - // nolint: bodyclose - conn, _, err := websocket.Dial(ctx, u.String(), nil) + server, err := agent.NewServer(agent.ServerArgs{ + Log: log, + CoderURL: u, + Token: token, + }) if err != nil { - return fmt.Errorf("dial: %w", err) + return xerrors.Errorf("creating agent server: %w", err) } - nc := websocket.NetConn(context.Background(), conn, websocket.MessageBinary) - session, err := yamux.Server(nc, nil) - if err != nil { - return fmt.Errorf("open: %w", err) - } - log.Info(ctx, "connected to broker. awaiting connection requests") - for { - st, err := session.AcceptStream() - if err != nil { - return fmt.Errorf("accept stream: %w", err) - } - stream := &stream{ - logger: log.Named(fmt.Sprintf("stream %d", st.StreamID())), - stream: st, - } - go stream.listen() + + err = server.Run(ctx) + if err != nil && !xerrors.Is(err, context.Canceled) && !xerrors.Is(err, context.DeadlineExceeded) { + return xerrors.Errorf("running agent server: %w", err) } + + return nil }, } @@ -134,157 +97,3 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx return cmd } - -type stream struct { - stream *yamux.Stream - logger slog.Logger - - rtc *webrtc.PeerConnection -} - -// writes an error and closes. -func (s *stream) fatal(err error) { - _ = s.write(proto.Message{ - Error: err.Error(), - }) - s.logger.Error(context.Background(), err.Error(), slog.Error(err)) - _ = s.stream.Close() -} - -func (s *stream) listen() { - decoder := json.NewDecoder(s.stream) - for { - var msg proto.Message - err := decoder.Decode(&msg) - if err == io.EOF { - break - } - if err != nil { - s.fatal(err) - return - } - s.processMessage(msg) - } -} - -func (s *stream) write(msg proto.Message) error { - d, err := json.Marshal(&msg) - if err != nil { - return err - } - _, err = s.stream.Write(d) - if err != nil { - return err - } - return nil -} - -func (s *stream) processMessage(msg proto.Message) { - s.logger.Debug(context.Background(), "processing message", slog.F("msg", msg)) - - if msg.Error != "" { - s.fatal(xerrors.New(msg.Error)) - return - } - - if msg.Candidate != "" { - if s.rtc == nil { - s.fatal(xerrors.New("rtc connection must be started before candidates are sent")) - return - } - - s.logger.Debug(context.Background(), "accepted ice candidate", slog.F("candidate", msg.Candidate)) - err := proto.AcceptICECandidate(s.rtc, &msg) - if err != nil { - s.fatal(err) - return - } - } - - if msg.Offer != nil { - rtc, err := xwebrtc.NewPeerConnection() - if err != nil { - s.fatal(fmt.Errorf("create connection: %w", err)) - return - } - flushCandidates := proto.ProxyICECandidates(rtc, s.stream) - - err = rtc.SetRemoteDescription(*msg.Offer) - if err != nil { - s.fatal(fmt.Errorf("set remote desc: %w", err)) - return - } - answer, err := rtc.CreateAnswer(nil) - if err != nil { - s.fatal(fmt.Errorf("create answer: %w", err)) - return - } - err = rtc.SetLocalDescription(answer) - if err != nil { - s.fatal(fmt.Errorf("set local desc: %w", err)) - return - } - flushCandidates() - - err = s.write(proto.Message{ - Answer: rtc.LocalDescription(), - }) - if err != nil { - s.fatal(fmt.Errorf("send local desc: %w", err)) - return - } - - rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { - s.logger.Info(context.Background(), "state changed", slog.F("new", pcs)) - }) - rtc.OnDataChannel(s.processDataChannel) - s.rtc = rtc - } -} - -func (s *stream) processDataChannel(channel *webrtc.DataChannel) { - if channel.Protocol() == "ping" { - channel.OnOpen(func() { - rw, err := channel.Detach() - if err != nil { - return - } - d := make([]byte, 64) - _, _ = rw.Read(d) - _, _ = rw.Write(d) - }) - return - } - - prto, port, err := xwebrtc.ParseProxyDataChannel(channel) - if err != nil { - s.fatal(fmt.Errorf("failed to parse proxy data channel: %w", err)) - return - } - if prto != "tcp" { - s.fatal(fmt.Errorf("client provided unsupported protocol: %s", prto)) - return - } - - conn, err := net.Dial(prto, fmt.Sprintf("localhost:%d", port)) - if err != nil { - s.fatal(fmt.Errorf("failed to dial client port: %d", port)) - return - } - - channel.OnOpen(func() { - s.logger.Debug(context.Background(), "proxying data channel to local port", slog.F("port", port)) - rw, err := channel.Detach() - if err != nil { - _ = channel.Close() - s.logger.Error(context.Background(), "detach client data channel", slog.Error(err)) - return - } - go func() { - _, _ = io.Copy(rw, conn) - }() - go func() { - _, _ = io.Copy(conn, rw) - }() - }) -} From 6a76b7d92f1c657072a7de26a5d174fc7cae46dc Mon Sep 17 00:00:00 2001 From: G r e y Date: Mon, 19 Apr 2021 15:16:03 -0500 Subject: [PATCH 005/128] feat: Remove autostart ed notice (#319) --- internal/cmd/envs.go | 22 ---------------------- internal/cmd/rebuild.go | 3 --- 2 files changed, 25 deletions(-) diff --git a/internal/cmd/envs.go b/internal/cmd/envs.go index 4a0c0101..c33db37d 100644 --- a/internal/cmd/envs.go +++ b/internal/cmd/envs.go @@ -10,7 +10,6 @@ import ( "cdr.dev/coder-cli/coder-sdk" "cdr.dev/coder-cli/internal/coderutil" - "cdr.dev/coder-cli/internal/config" "cdr.dev/coder-cli/internal/x/xcobra" "cdr.dev/coder-cli/pkg/clog" "cdr.dev/coder-cli/pkg/tablewriter" @@ -184,9 +183,6 @@ func createEnvCmd() *cobra.Command { Example: `# create a new environment using default resource amounts coder envs create my-new-env --image ubuntu coder envs create my-new-powerful-env --cpu 12 --disk 100 --memory 16 --image ubuntu`, - PreRun: func(cmd *cobra.Command, args []string) { - autoStartInfo() - }, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() if img == "" { @@ -440,9 +436,6 @@ func editEnvCmd() *cobra.Command { Example: `coder envs edit back-end-env --cpu 4 coder envs edit back-end-env --disk 20`, - PreRun: func(cmd *cobra.Command, args []string) { - autoStartInfo() - }, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() client, err := newClient(ctx) @@ -684,18 +677,3 @@ func buildUpdateReq(ctx context.Context, client coder.Client, conf updateConf) ( } return &updateReq, nil } - -// TODO (Grey): Remove education in a future non-patch release. -func autoStartInfo() { - var preferencesURI string - - accessURI, err := config.URL.Read() - if err != nil { - // Error is fairly benign in this case, fallback to relative URI - preferencesURI = "/preferences" - } else { - preferencesURI = fmt.Sprintf("%s%s", accessURI, "/preferences?tab=autostart") - } - - clog.LogInfo("⚡NEW: Automate daily environment startup", "Visit "+preferencesURI+" to configure your preferred time") -} diff --git a/internal/cmd/rebuild.go b/internal/cmd/rebuild.go index 8cd6ca2e..fe6c3dff 100644 --- a/internal/cmd/rebuild.go +++ b/internal/cmd/rebuild.go @@ -27,9 +27,6 @@ func rebuildEnvCommand() *cobra.Command { Args: xcobra.ExactArgs(1), Example: `coder envs rebuild front-end-env --follow coder envs rebuild backend-env --force`, - PreRun: func(cmd *cobra.Command, args []string) { - autoStartInfo() - }, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() client, err := newClient(ctx) From eccaa79b59b5fd81faba26382cebdd67d60ce207 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 21 Apr 2021 09:52:27 -0500 Subject: [PATCH 006/128] Add coder providers rename command (#320) * Add coder providers rename command * Update interface.go --- coder-sdk/interface.go | 3 +++ coder-sdk/workspace_providers.go | 15 +++++++++++++++ internal/cmd/providers.go | 33 ++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/coder-sdk/interface.go b/coder-sdk/interface.go index 93043ee7..ef9995c1 100644 --- a/coder-sdk/interface.go +++ b/coder-sdk/interface.go @@ -232,4 +232,7 @@ type Client interface { // UnCordonWorkspaceProvider changes an existing cordoned providers status to 'Ready'; // allowing it to continue creating new workspaces and provisioning resources for them. UnCordonWorkspaceProvider(ctx context.Context, id string) error + + // RenameWorkspaceProvider changes an existing providers name field. + RenameWorkspaceProvider(ctx context.Context, id string, name string) error } diff --git a/coder-sdk/workspace_providers.go b/coder-sdk/workspace_providers.go index 673dc63e..28710867 100644 --- a/coder-sdk/workspace_providers.go +++ b/coder-sdk/workspace_providers.go @@ -123,3 +123,18 @@ func (c *DefaultClient) UnCordonWorkspaceProvider(ctx context.Context, id string } return nil } + +// RenameWorkspaceProviderReq defines the request parameters for changing a workspace provider name. +type RenameWorkspaceProviderReq struct { + Name string `json:"name"` +} + +// RenameWorkspaceProvider changes an existing cordoned providers name field. +func (c *DefaultClient) RenameWorkspaceProvider(ctx context.Context, id string, name string) error { + req := RenameWorkspaceProviderReq{Name: name} + err := c.requestBody(ctx, http.MethodPatch, "/api/private/resource-pools/"+id, req, nil) + if err != nil { + return err + } + return nil +} diff --git a/internal/cmd/providers.go b/internal/cmd/providers.go index 678327d3..46074933 100644 --- a/internal/cmd/providers.go +++ b/internal/cmd/providers.go @@ -29,6 +29,7 @@ func providersCmd() *cobra.Command { deleteProviderCmd(), cordonProviderCmd(), unCordonProviderCmd(), + renameProviderCmd(), ) return cmd } @@ -284,3 +285,35 @@ coder providers uncordon my-workspace-provider`, } return cmd } + +func renameProviderCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "rename [old_name] [new_name]", + Args: xcobra.ExactArgs(2), + Short: "rename a workspace provider.", + Long: "Changes the name field of an existing workspace provider.", + Example: `# rename a workspace provider from 'built-in' to 'us-east-1' +coder providers rename build-in us-east-1`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + client, err := newClient(ctx) + if err != nil { + return err + } + + oldName := args[0] + newName := args[1] + provider, err := coderutil.ProviderByName(ctx, client, oldName) + if err != nil { + return err + } + + if err := client.RenameWorkspaceProvider(ctx, provider.ID, newName); err != nil { + return err + } + clog.LogSuccess(fmt.Sprintf("provider %s successfully renamed to %s", oldName, newName)) + return nil + }, + } + return cmd +} From 980b331d4307b3db5b720da37127d4e1a6ad612b Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 21 Apr 2021 21:43:33 -0500 Subject: [PATCH 007/128] chore: unmarshal HTTP errors for p2p commands (#321) --- .golangci.yml | 6 ++-- agent/server.go | 64 +++++++++++++++++++++++++++--------------- internal/cmd/tunnel.go | 10 +++++-- 3 files changed, 53 insertions(+), 27 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 423540d5..4bde9808 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,7 +4,9 @@ linters-settings: min-len: 4 min-occurrences: 3 gocognit: - min-complexity: 46 + # tunnel.go has a 150 line function. Someone should fix it and + # decrement this back down to a rational number. + min-complexity: 52 nestif: min-complexity: 10 govet: @@ -68,4 +70,4 @@ issues: # gosec: Too many issues in popular repos - (Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less) # gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)' - - Potential file inclusion via variable \ No newline at end of file + - Potential file inclusion via variable diff --git a/agent/server.go b/agent/server.go index 17bb51f7..33a33fdb 100644 --- a/agent/server.go +++ b/agent/server.go @@ -11,6 +11,8 @@ import ( "go.coder.com/retry" "golang.org/x/xerrors" "nhooyr.io/websocket" + + "cdr.dev/coder-cli/coder-sdk" ) const ( @@ -45,32 +47,48 @@ func NewServer(args ServerArgs) (*Server, error) { // Run will listen and proxy new peer connections on a retry loop. func (s *Server) Run(ctx context.Context) error { - err := retry.New(time.Second).Context(ctx).Backoff(15 * time.Second).Run(func() error { - ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) - defer cancelFunc() - s.log.Info(ctx, "connecting to coder", slog.F("url", s.listenURL.String())) - conn, _, err := websocket.Dial(ctx, s.listenURL.String(), nil) - if err != nil { - return fmt.Errorf("dial: %w", err) - } - nc := websocket.NetConn(context.Background(), conn, websocket.MessageBinary) - session, err := yamux.Server(nc, nil) - if err != nil { - return fmt.Errorf("open: %w", err) - } - s.log.Info(ctx, "connected to coder. awaiting connection requests") - for { - st, err := session.AcceptStream() + err := retry.New(time.Second). + Context(ctx). + Backoff(15 * time.Second). + Conditions( + retry.Condition(func(err error) bool { + if err != nil { + s.log.Error(ctx, "failed to connect", slog.Error(err)) + } + return true + }), + ).Run( + func() error { + ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) + defer cancelFunc() + s.log.Info(ctx, "connecting to coder", slog.F("url", s.listenURL.String())) + conn, resp, err := websocket.Dial(ctx, s.listenURL.String(), nil) + if err != nil && resp == nil { + return fmt.Errorf("dial: %w", err) + } + if err != nil && resp != nil { + return &coder.HTTPError{ + Response: resp, + } + } + nc := websocket.NetConn(context.Background(), conn, websocket.MessageBinary) + session, err := yamux.Server(nc, nil) if err != nil { - return fmt.Errorf("accept stream: %w", err) + return fmt.Errorf("open: %w", err) } - stream := &stream{ - logger: s.log.Named(fmt.Sprintf("stream %d", st.StreamID())), - stream: st, + s.log.Info(ctx, "connected to coder. awaiting connection requests") + for { + st, err := session.AcceptStream() + if err != nil { + return fmt.Errorf("accept stream: %w", err) + } + stream := &stream{ + logger: s.log.Named(fmt.Sprintf("stream %d", st.StreamID())), + stream: st, + } + go stream.listen() } - go stream.listen() - } - }) + }) return err } diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index fbfd0a1c..97c03c1c 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -17,6 +17,7 @@ import ( "golang.org/x/xerrors" "nhooyr.io/websocket" + "cdr.dev/coder-cli/coder-sdk" "cdr.dev/coder-cli/internal/x/xcobra" "cdr.dev/coder-cli/internal/x/xwebrtc" "cdr.dev/coder-cli/pkg/proto" @@ -110,10 +111,15 @@ func (c *client) start() error { url := fmt.Sprintf("%s%s%s%s%s", c.brokerAddr, "/api/private/envagent/", c.id, "/connect?session_token=", c.token) c.logger.Info(c.ctx, "connecting to broker", slog.F("url", url)) - conn, _, err := websocket.Dial(c.ctx, url, nil) - if err != nil { + conn, resp, err := websocket.Dial(c.ctx, url, nil) + if err != nil && resp == nil { return fmt.Errorf("dial: %w", err) } + if err != nil && resp != nil { + return &coder.HTTPError{ + Response: resp, + } + } nconn := websocket.NetConn(context.Background(), conn, websocket.MessageBinary) rtc, err := xwebrtc.NewPeerConnection() From 8189483bbc90314d7196f262e8033a104b08572e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Apr 2021 09:16:00 -0500 Subject: [PATCH 008/128] chore: bump github.com/pion/webrtc/v3 from 3.0.20 to 3.0.24 (#324) Bumps [github.com/pion/webrtc/v3](https://github.com/pion/webrtc) from 3.0.20 to 3.0.24. - [Release notes](https://github.com/pion/webrtc/releases) - [Commits](https://github.com/pion/webrtc/compare/v3.0.20...v3.0.24) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 61 ++++++++++++++++++++++++++++++++++------------------------ 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index 1a88ad94..08f68622 100644 --- a/go.mod +++ b/go.mod @@ -13,13 +13,13 @@ require ( github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f github.com/klauspost/compress v1.10.8 // indirect github.com/manifoldco/promptui v0.8.0 - github.com/pion/webrtc/v3 v3.0.20 + github.com/pion/webrtc/v3 v3.0.24 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.1.3 go.coder.com/retry v1.2.0 - golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 - golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 + golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 diff --git a/go.sum b/go.sum index 0ec0d443..64d62da5 100644 --- a/go.sum +++ b/go.sum @@ -100,6 +100,7 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= @@ -127,8 +128,9 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -244,21 +246,22 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0= github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= -github.com/pion/dtls/v2 v2.0.8 h1:reGe8rNIMfO/UAeFLqO61tl64t154Qfkr4U3Gzu1tsg= -github.com/pion/dtls/v2 v2.0.8/go.mod h1:QuDII+8FVvk9Dp5t5vYIMTo7hh7uBkra+8QIm7QGm10= -github.com/pion/ice/v2 v2.0.16 h1:K6bzD8ef9vMKbGMTHaUweHXEyuNGnvr2zdqKoLKZPn0= -github.com/pion/ice/v2 v2.0.16/go.mod h1:SJNJzC27gDZoOW0UoxIoC8Hf2PDxG28hQyNdSexDu38= +github.com/pion/dtls/v2 v2.0.9 h1:7Ow+V++YSZQMYzggI0P9vLJz/hUFcffsfGMfT/Qy+u8= +github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQKYQho= +github.com/pion/ice/v2 v2.1.5 h1:OqWkKmiLqDU6j+oZnoaxTtO2tR/2QwrUfj3l7BgltzE= +github.com/pion/ice/v2 v2.1.5/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= github.com/pion/interceptor v0.0.12 h1:eC1iVneBIAQJEfaNAfDqAncJWhMDAnaXPRCJsltdokE= github.com/pion/interceptor v0.0.12/go.mod h1:qzeuWuD/ZXvPqOnxNcnhWfkCZ2e1kwwslicyyPnhoK4= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= @@ -280,17 +283,16 @@ github.com/pion/srtp/v2 v2.0.2 h1:664iGzVmaY7KYS5M0gleY0DscRo9ReDfTxQrq4UgGoU= github.com/pion/srtp/v2 v2.0.2/go.mod h1:VEyLv4CuxrwGY8cxM+Ng3bmVy8ckz/1t6A0q/msKOw0= github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= -github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE= github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= github.com/pion/transport v0.12.3 h1:vdBfvfU/0Wq8kd2yhUMSDB/x+O4Z9MYVl2fJ5BT4JZw= github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A= github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= -github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI= -github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= -github.com/pion/webrtc/v3 v3.0.20 h1:Jj0sk45MqQdkR24E1wbFRmOzb1Lv258ot9zd2fYB/Pw= -github.com/pion/webrtc/v3 v3.0.20/go.mod h1:0eJnCpQrUMpRnvyonw4ZiWClToerpixrZ2KcoTxvX9M= +github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= +github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= +github.com/pion/webrtc/v3 v3.0.24 h1:IfWd+W7RwISNxK06HolobsXO+qs61xOBzca2cq+RlkE= +github.com/pion/webrtc/v3 v3.0.24/go.mod h1:Qx9zd4xvIeFTN1hygyJ77XVi/YbElyjVitL6KyCEIpE= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -355,6 +357,7 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.coder.com/cli v0.4.0/go.mod h1:hRTOURCR3LJF1FRW9arecgrzX+AHG7mfYMwThPIgq+w= go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512 h1:DjCS6dRQh+1PlfiBmnabxfdrzenb0tAwJqFxDEH/s9g= go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512/go.mod h1:83JsYgXYv0EOaXjIMnaZ1Fl6ddNB3fJnDZ/8845mUJ8= @@ -373,10 +376,11 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -395,6 +399,7 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -411,14 +416,16 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 h1:b0LrWgu8+q7z4J+0Y3Umo5q1dL7NXBkKBWkaVkAq17E= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210420210106-798c2154c571 h1:Q6Bg8xzKzpFPU4Oi1sBnBTHBwlMsLeEXpu4hYBY8rAg= +golang.org/x/net v0.0.0-20210420210106-798c2154c571/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -427,8 +434,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -455,19 +462,20 @@ golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005 h1:pDMpM2zh2MT0kHy037cKlSby2nEhD50SYqwQk76Nm40= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe h1:WdX7u8s3yOigWAhHEaDl8r9G+4XwFQEQFtBMYyN+kXQ= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= @@ -491,7 +499,10 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 51ddb08046131b9aec51ddf9f915a55aa2a6dff3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Apr 2021 14:18:05 +0000 Subject: [PATCH 009/128] chore: bump nhooyr.io/websocket from 1.8.6 to 1.8.7 (#312) Bumps [nhooyr.io/websocket](https://github.com/nhooyr/websocket) from 1.8.6 to 1.8.7. - [Release notes](https://github.com/nhooyr/websocket/releases) - [Commits](https://github.com/nhooyr/websocket/compare/v1.8.6...v1.8.7) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 08f68622..81a93226 100644 --- a/go.mod +++ b/go.mod @@ -23,5 +23,5 @@ require ( golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 - nhooyr.io/websocket v1.8.6 + nhooyr.io/websocket v1.8.7 ) diff --git a/go.sum b/go.sum index 64d62da5..2252f265 100644 --- a/go.sum +++ b/go.sum @@ -566,6 +566,7 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= From 7adf4a2281c9c57df1154aeb403f95fc9bbf6065 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Apr 2021 14:18:34 +0000 Subject: [PATCH 010/128] chore: bump golangci/golangci-lint-action from v2 to v2.5.2 (#309) Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from v2 to v2.5.2. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/v2...5c56cd6c9dc07901af25baab6f2b0d9f3b7c3018) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9a3261ea..a3cf3c60 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v2.5.2 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. version: v1.36 From 43edc2ff7ab57792a1352fc0ff1c76aa29c0d6af Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 23 Apr 2021 09:22:12 -0500 Subject: [PATCH 011/128] feat: Use internal TURN server for proxying (#322) * feat: Use internal TURN server for proxying * Use TURNS by default * Adjust TURN scheme when brokerAddr is HTTP or not --- agent/stream.go | 6 +++++- internal/cmd/tunnel.go | 34 ++++++++++++++++++++++++---------- internal/x/xwebrtc/conn.go | 15 ++++++++------- pkg/proto/message.go | 1 + 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/agent/stream.go b/agent/stream.go index 42a2233f..01a10f77 100644 --- a/agent/stream.go +++ b/agent/stream.go @@ -83,7 +83,11 @@ func (s *stream) processMessage(msg proto.Message) { } if msg.Offer != nil { - rtc, err := xwebrtc.NewPeerConnection() + if msg.Servers == nil { + s.fatal(fmt.Errorf("servers must be sent with offer")) + return + } + rtc, err := xwebrtc.NewPeerConnection(msg.Servers) if err != nil { s.fatal(fmt.Errorf("create connection: %w", err)) return diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index 97c03c1c..c1a56621 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net" + "net/url" "os" "strconv" "time" @@ -57,7 +58,7 @@ coder tunnel my-dev 3000 3000 } baseURL := sdk.BaseURL() - envs, err := sdk.Environments(ctx) + envs, err := getEnvs(ctx, sdk, coder.Me) if err != nil { return err } @@ -79,8 +80,8 @@ coder tunnel my-dev 3000 3000 localPort: uint16(localPort), remotePort: uint16(remotePort), ctx: context.Background(), - logger: log, - brokerAddr: baseURL.String(), + logger: log.Leveled(slog.LevelDebug), + brokerAddr: baseURL, token: sdk.Token(), } @@ -98,7 +99,7 @@ coder tunnel my-dev 3000 3000 type client struct { ctx context.Context - brokerAddr string + brokerAddr url.URL token string logger slog.Logger id string @@ -108,9 +109,13 @@ type client struct { } func (c *client) start() error { - url := fmt.Sprintf("%s%s%s%s%s", c.brokerAddr, "/api/private/envagent/", c.id, "/connect?session_token=", c.token) - c.logger.Info(c.ctx, "connecting to broker", slog.F("url", url)) - + url := fmt.Sprintf("%s%s%s%s%s", c.brokerAddr.String(), "/api/private/envagent/", c.id, "/connect?session_token=", c.token) + turnScheme := "turns" + if c.brokerAddr.Scheme == "http" { + turnScheme = "turn" + } + tcpProxy := fmt.Sprintf("%s:%s:5349?transport=tcp", turnScheme, c.brokerAddr.Host) + c.logger.Info(c.ctx, "connecting to broker", slog.F("url", url), slog.F("tcp-proxy", tcpProxy)) conn, resp, err := websocket.Dial(c.ctx, url, nil) if err != nil && resp == nil { return fmt.Errorf("dial: %w", err) @@ -122,7 +127,15 @@ func (c *client) start() error { } nconn := websocket.NetConn(context.Background(), conn, websocket.MessageBinary) - rtc, err := xwebrtc.NewPeerConnection() + // Only enabled under a private feature flag for now, + // so insecure connections are entirely fine to allow. + servers := []webrtc.ICEServer{{ + URLs: []string{tcpProxy}, + Username: "insecure", + Credential: "pass", + CredentialType: webrtc.ICECredentialTypePassword, + }} + rtc, err := xwebrtc.NewPeerConnection(servers) if err != nil { return fmt.Errorf("create connection: %w", err) } @@ -150,16 +163,17 @@ func (c *client) start() error { if err != nil { return fmt.Errorf("set local desc: %w", err) } - flushCandidates() c.logger.Debug(context.Background(), "writing offer") b, _ := json.Marshal(&proto.Message{ - Offer: &localDesc, + Offer: &localDesc, + Servers: servers, }) _, err = nconn.Write(b) if err != nil { return fmt.Errorf("write offer: %w", err) } + flushCandidates() go func() { err = xwebrtc.WaitForDataChannelOpen(context.Background(), channel) diff --git a/internal/x/xwebrtc/conn.go b/internal/x/xwebrtc/conn.go index 20178f2e..5237cb8f 100644 --- a/internal/x/xwebrtc/conn.go +++ b/internal/x/xwebrtc/conn.go @@ -1,19 +1,20 @@ package xwebrtc -import "github.com/pion/webrtc/v3" +import ( + "time" + + "github.com/pion/webrtc/v3" +) // NewPeerConnection creates a new peer connection. // It uses the Google stun server by default. -func NewPeerConnection() (*webrtc.PeerConnection, error) { +func NewPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, error) { se := webrtc.SettingEngine{} se.DetachDataChannels() + se.SetICETimeouts(time.Second*5, time.Second*5, time.Second*2) api := webrtc.NewAPI(webrtc.WithSettingEngine(se)) return api.NewPeerConnection(webrtc.Configuration{ - ICEServers: []webrtc.ICEServer{ - { - URLs: []string{"stun:stun.l.google.com:19302"}, - }, - }, + ICEServers: servers, }) } diff --git a/pkg/proto/message.go b/pkg/proto/message.go index b6d7876d..55f5d91f 100644 --- a/pkg/proto/message.go +++ b/pkg/proto/message.go @@ -13,6 +13,7 @@ type Message struct { Error string `json:"error"` Candidate string `json:"candidate"` Offer *webrtc.SessionDescription `json:"offer"` + Servers []webrtc.ICEServer `json:"servers"` Answer *webrtc.SessionDescription `json:"answer"` } From 9adbf4fcd090fb6571b5524b85f6e8b126c64c04 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 28 Apr 2021 20:16:32 -0500 Subject: [PATCH 012/128] Centralize webrtc dial logic into xwebrtc (#326) --- agent/server.go | 5 +- agent/stream.go | 15 +- go.mod | 1 + internal/cmd/tunnel.go | 223 +++++-------------------- internal/x/xwebrtc/channel.go | 56 ------- internal/x/xwebrtc/conn.go | 20 --- xwebrtc/channel.go | 76 +++++++++ xwebrtc/conn.go | 80 +++++++++ xwebrtc/dialer.go | 195 +++++++++++++++++++++ {internal/x/xwebrtc => xwebrtc}/doc.go | 0 10 files changed, 403 insertions(+), 268 deletions(-) delete mode 100644 internal/x/xwebrtc/channel.go delete mode 100644 internal/x/xwebrtc/conn.go create mode 100644 xwebrtc/channel.go create mode 100644 xwebrtc/conn.go create mode 100644 xwebrtc/dialer.go rename {internal/x/xwebrtc => xwebrtc}/doc.go (100%) diff --git a/agent/server.go b/agent/server.go index 33a33fdb..73d399a2 100644 --- a/agent/server.go +++ b/agent/server.go @@ -59,8 +59,6 @@ func (s *Server) Run(ctx context.Context) error { }), ).Run( func() error { - ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) - defer cancelFunc() s.log.Info(ctx, "connecting to coder", slog.F("url", s.listenURL.String())) conn, resp, err := websocket.Dial(ctx, s.listenURL.String(), nil) if err != nil && resp == nil { @@ -71,7 +69,8 @@ func (s *Server) Run(ctx context.Context) error { Response: resp, } } - nc := websocket.NetConn(context.Background(), conn, websocket.MessageBinary) + + nc := websocket.NetConn(ctx, conn, websocket.MessageBinary) session, err := yamux.Server(nc, nil) if err != nil { return fmt.Errorf("open: %w", err) diff --git a/agent/stream.go b/agent/stream.go index 01a10f77..4a940f5a 100644 --- a/agent/stream.go +++ b/agent/stream.go @@ -7,12 +7,13 @@ import ( "io" "net" + "cdr.dev/coder-cli/xwebrtc" + "cdr.dev/slog" "github.com/hashicorp/yamux" "github.com/pion/webrtc/v3" "golang.org/x/xerrors" - "cdr.dev/coder-cli/internal/x/xwebrtc" "cdr.dev/coder-cli/pkg/proto" ) @@ -128,6 +129,10 @@ func (s *stream) processMessage(msg proto.Message) { } func (s *stream) processDataChannel(channel *webrtc.DataChannel) { + if channel.Protocol() == "control" { + return + } + if channel.Protocol() == "ping" { channel.OnOpen(func() { rw, err := channel.Detach() @@ -149,7 +154,7 @@ func (s *stream) processDataChannel(channel *webrtc.DataChannel) { return } - prto, port, err := xwebrtc.ParseProxyDataChannel(channel) + prto, addr, err := xwebrtc.ParseProxyDataChannel(channel) if err != nil { s.fatal(fmt.Errorf("failed to parse proxy data channel: %w", err)) return @@ -159,14 +164,14 @@ func (s *stream) processDataChannel(channel *webrtc.DataChannel) { return } - conn, err := net.Dial(prto, fmt.Sprintf("localhost:%d", port)) + conn, err := net.Dial(prto, addr) if err != nil { - s.fatal(fmt.Errorf("failed to dial client port: %d", port)) + s.fatal(fmt.Errorf("failed to dial client addr: %s", addr)) return } channel.OnOpen(func() { - s.logger.Debug(context.Background(), "proxying data channel to local port", slog.F("port", port)) + s.logger.Debug(context.Background(), "proxying data channel", slog.F("addr", addr)) rw, err := channel.Detach() if err != nil { _ = channel.Close() diff --git a/go.mod b/go.mod index 81a93226..d3129a91 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.1.3 go.coder.com/retry v1.2.0 + golang.org/x/net v0.0.0-20210420210106-798c2154c571 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index c1a56621..ec8e179d 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -2,26 +2,21 @@ package cmd import ( "context" - "encoding/json" "fmt" "io" "net" "net/url" "os" "strconv" - "time" "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" - "github.com/pion/webrtc/v3" "github.com/spf13/cobra" "golang.org/x/xerrors" - "nhooyr.io/websocket" "cdr.dev/coder-cli/coder-sdk" "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/internal/x/xwebrtc" - "cdr.dev/coder-cli/pkg/proto" + "cdr.dev/coder-cli/xwebrtc" ) func tunnelCmd() *cobra.Command { @@ -41,26 +36,26 @@ coder tunnel my-dev 3000 3000 remotePort, err := strconv.ParseUint(args[1], 10, 16) if err != nil { - log.Fatal(ctx, "parse remote port", slog.Error(err)) + return xerrors.Errorf("parse remote port: %w", err) } var localPort uint64 if args[2] != "stdio" { localPort, err = strconv.ParseUint(args[2], 10, 16) if err != nil { - log.Fatal(ctx, "parse local port", slog.Error(err)) + return xerrors.Errorf("parse local port: %w", err) } } sdk, err := newClient(ctx) if err != nil { - return err + return xerrors.Errorf("getting coder client: %w", err) } baseURL := sdk.BaseURL() envs, err := getEnvs(ctx, sdk, coder.Me) if err != nil { - return err + return xerrors.Errorf("get workspaces: %w", err) } var envID string @@ -74,20 +69,19 @@ coder tunnel my-dev 3000 3000 return xerrors.Errorf("No workspace found by name '%s'", args[0]) } - c := &client{ - id: envID, - stdio: args[2] == "stdio", - localPort: uint16(localPort), - remotePort: uint16(remotePort), - ctx: context.Background(), - logger: log.Leveled(slog.LevelDebug), - brokerAddr: baseURL, - token: sdk.Token(), + c := &tunnneler{ + log: log.Leveled(slog.LevelDebug), + brokerAddr: &baseURL, + token: sdk.Token(), + workspaceID: envID, + stdio: args[2] == "stdio", + localPort: uint16(localPort), + remotePort: uint16(remotePort), } - err = c.start() + err = c.start(ctx) if err != nil { - log.Fatal(ctx, err.Error()) + return xerrors.Errorf("running tunnel: %w", err) } return nil @@ -97,197 +91,58 @@ coder tunnel my-dev 3000 3000 return cmd } -type client struct { - ctx context.Context - brokerAddr url.URL - token string - logger slog.Logger - id string - remotePort uint16 - localPort uint16 - stdio bool +type tunnneler struct { + log slog.Logger + brokerAddr *url.URL + token string + workspaceID string + remotePort uint16 + localPort uint16 + stdio bool } -func (c *client) start() error { - url := fmt.Sprintf("%s%s%s%s%s", c.brokerAddr.String(), "/api/private/envagent/", c.id, "/connect?session_token=", c.token) - turnScheme := "turns" - if c.brokerAddr.Scheme == "http" { - turnScheme = "turn" - } - tcpProxy := fmt.Sprintf("%s:%s:5349?transport=tcp", turnScheme, c.brokerAddr.Host) - c.logger.Info(c.ctx, "connecting to broker", slog.F("url", url), slog.F("tcp-proxy", tcpProxy)) - conn, resp, err := websocket.Dial(c.ctx, url, nil) - if err != nil && resp == nil { - return fmt.Errorf("dial: %w", err) - } - if err != nil && resp != nil { - return &coder.HTTPError{ - Response: resp, - } - } - nconn := websocket.NetConn(context.Background(), conn, websocket.MessageBinary) - - // Only enabled under a private feature flag for now, - // so insecure connections are entirely fine to allow. - servers := []webrtc.ICEServer{{ - URLs: []string{tcpProxy}, - Username: "insecure", - Credential: "pass", - CredentialType: webrtc.ICECredentialTypePassword, - }} - rtc, err := xwebrtc.NewPeerConnection(servers) - if err != nil { - return fmt.Errorf("create connection: %w", err) - } - - rtc.OnNegotiationNeeded(func() { - c.logger.Debug(context.Background(), "negotiation needed...") - }) - - rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { - c.logger.Info(context.Background(), "connection state changed", slog.F("state", pcs)) - }) - - channel, err := xwebrtc.NewProxyDataChannel(rtc, "forwarder", "tcp", c.remotePort) - if err != nil { - return fmt.Errorf("create data channel: %w", err) - } - flushCandidates := proto.ProxyICECandidates(rtc, nconn) - - localDesc, err := rtc.CreateOffer(&webrtc.OfferOptions{}) - if err != nil { - return fmt.Errorf("create offer: %w", err) - } - - err = rtc.SetLocalDescription(localDesc) - if err != nil { - return fmt.Errorf("set local desc: %w", err) - } - - c.logger.Debug(context.Background(), "writing offer") - b, _ := json.Marshal(&proto.Message{ - Offer: &localDesc, - Servers: servers, - }) - _, err = nconn.Write(b) +func (c *tunnneler) start(ctx context.Context) error { + wd, err := xwebrtc.NewWorkspaceDialer(ctx, c.log, c.brokerAddr, c.token, c.workspaceID) if err != nil { - return fmt.Errorf("write offer: %w", err) - } - flushCandidates() - - go func() { - err = xwebrtc.WaitForDataChannelOpen(context.Background(), channel) - if err != nil { - c.logger.Fatal(context.Background(), "waiting for data channel open", slog.Error(err)) - } - _ = conn.Close(websocket.StatusNormalClosure, "rtc connected") - }() - - decoder := json.NewDecoder(nconn) - for { - var msg proto.Message - err = decoder.Decode(&msg) - if err == io.EOF { - break - } - if websocket.CloseStatus(err) == websocket.StatusNormalClosure { - break - } - if err != nil { - return fmt.Errorf("read msg: %w", err) - } - if msg.Candidate != "" { - c.logger.Debug(context.Background(), "accepted ice candidate", slog.F("candidate", msg.Candidate)) - err = proto.AcceptICECandidate(rtc, &msg) - if err != nil { - return fmt.Errorf("accept ice: %w", err) - } - } - if msg.Answer != nil { - c.logger.Debug(context.Background(), "got answer", slog.F("answer", msg.Answer)) - err = rtc.SetRemoteDescription(*msg.Answer) - if err != nil { - return fmt.Errorf("set remote: %w", err) - } - } + return xerrors.Errorf("creating workspace dialer: %w", wd) } - - // Once we're open... let's test out the ping. - pingProto := "ping" - pingChannel, err := rtc.CreateDataChannel("pinger", &webrtc.DataChannelInit{ - Protocol: &pingProto, - }) + nc, err := wd.DialContext(ctx, xwebrtc.NetworkTCP, fmt.Sprintf("localhost:%d", c.remotePort)) if err != nil { - return fmt.Errorf("create ping channel") + return xerrors.Errorf("dial: %w", err) } - pingChannel.OnOpen(func() { - defer func() { - _ = pingChannel.Close() - }() - t1 := time.Now() - rw, _ := pingChannel.Detach() - defer func() { - _ = rw.Close() - }() - _, _ = rw.Write([]byte("hello")) - b := make([]byte, 64) - _, _ = rw.Read(b) - c.logger.Info(c.ctx, "your latency directly to the agent", slog.F("ms", time.Since(t1).Milliseconds())) - }) + // proxy via stdio if c.stdio { - // At this point the RTC is connected and data channel is opened... - rw, err := channel.Detach() - if err != nil { - return fmt.Errorf("detach channel: %w", err) - } go func() { - _, _ = io.Copy(rw, os.Stdin) + _, _ = io.Copy(nc, os.Stdin) }() - _, err = io.Copy(os.Stdout, rw) + _, err = io.Copy(os.Stdout, nc) if err != nil { - return fmt.Errorf("copy: %w", err) + return xerrors.Errorf("copy: %w", err) } return nil } + // proxy via tcp listener listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", c.localPort)) if err != nil { - return fmt.Errorf("listen: %w", err) + return xerrors.Errorf("listen: %w", err) } for { - conn, err := listener.Accept() + lc, err := listener.Accept() if err != nil { - return fmt.Errorf("accept: %w", err) + return xerrors.Errorf("accept: %w", err) } go func() { defer func() { - _ = conn.Close() - }() - channel, err := xwebrtc.NewProxyDataChannel(rtc, "forwarder", "tcp", c.remotePort) - if err != nil { - c.logger.Warn(context.Background(), "create data channel for proxying", slog.Error(err)) - return - } - defer func() { - _ = channel.Close() + _ = lc.Close() }() - err = xwebrtc.WaitForDataChannelOpen(context.Background(), channel) - if err != nil { - c.logger.Warn(context.Background(), "wait for data channel open", slog.Error(err)) - return - } - rw, err := channel.Detach() - if err != nil { - c.logger.Warn(context.Background(), "detach channel", slog.Error(err)) - return - } go func() { - _, _ = io.Copy(conn, rw) + _, _ = io.Copy(lc, nc) }() - _, _ = io.Copy(rw, conn) + _, _ = io.Copy(nc, lc) }() } } diff --git a/internal/x/xwebrtc/channel.go b/internal/x/xwebrtc/channel.go deleted file mode 100644 index 08442d9e..00000000 --- a/internal/x/xwebrtc/channel.go +++ /dev/null @@ -1,56 +0,0 @@ -package xwebrtc - -import ( - "context" - "errors" - "fmt" - "net" - "strconv" - "time" - - "github.com/pion/webrtc/v3" -) - -// WaitForDataChannelOpen waits for the data channel to have the open state. -// By default, it waits 15 seconds. -func WaitForDataChannelOpen(ctx context.Context, channel *webrtc.DataChannel) error { - if channel.ReadyState() == webrtc.DataChannelStateOpen { - return nil - } - ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) - defer cancelFunc() - channel.OnOpen(func() { - cancelFunc() - }) - <-ctx.Done() - if ctx.Err() == context.DeadlineExceeded { - return ctx.Err() - } - return nil -} - -// NewProxyDataChannel creates a new data channel for proxying. -func NewProxyDataChannel(conn *webrtc.PeerConnection, name, protocol string, port uint16) (*webrtc.DataChannel, error) { - proto := fmt.Sprintf("%s:%d", protocol, port) - ordered := true - return conn.CreateDataChannel(name, &webrtc.DataChannelInit{ - Protocol: &proto, - Ordered: &ordered, - }) -} - -// ParseProxyDataChannel parses a data channel to get the protocol and port. -func ParseProxyDataChannel(channel *webrtc.DataChannel) (string, uint16, error) { - if channel.Protocol() == "" { - return "", 0, errors.New("data channel is not a proxy") - } - host, port, err := net.SplitHostPort(channel.Protocol()) - if err != nil { - return "", 0, fmt.Errorf("split protocol: %w", err) - } - p, err := strconv.ParseInt(port, 10, 16) - if err != nil { - return "", 0, fmt.Errorf("parse port: %w", err) - } - return host, uint16(p), nil -} diff --git a/internal/x/xwebrtc/conn.go b/internal/x/xwebrtc/conn.go deleted file mode 100644 index 5237cb8f..00000000 --- a/internal/x/xwebrtc/conn.go +++ /dev/null @@ -1,20 +0,0 @@ -package xwebrtc - -import ( - "time" - - "github.com/pion/webrtc/v3" -) - -// NewPeerConnection creates a new peer connection. -// It uses the Google stun server by default. -func NewPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, error) { - se := webrtc.SettingEngine{} - se.DetachDataChannels() - se.SetICETimeouts(time.Second*5, time.Second*5, time.Second*2) - api := webrtc.NewAPI(webrtc.WithSettingEngine(se)) - - return api.NewPeerConnection(webrtc.Configuration{ - ICEServers: servers, - }) -} diff --git a/xwebrtc/channel.go b/xwebrtc/channel.go new file mode 100644 index 00000000..35938a1d --- /dev/null +++ b/xwebrtc/channel.go @@ -0,0 +1,76 @@ +package xwebrtc + +import ( + "context" + "fmt" + "strings" + "time" + + "golang.org/x/xerrors" + + "github.com/pion/webrtc/v3" +) + +// ParseProxyDataChannel parses a data channel to get the network and addr. +func ParseProxyDataChannel(channel *webrtc.DataChannel) (string, string, error) { + if channel.Protocol() == "" { + return "", "", xerrors.New("data channel is not a proxy") + } + segments := strings.SplitN(channel.Protocol(), ":", 2) + if len(segments) != 2 { + return "", "", xerrors.Errorf("protocol is malformed: %s", channel.Protocol()) + } + + return segments[0], segments[1], nil +} + +// NewPeerConnection creates a new peer connection. +// It uses the Google stun server by default. +func NewPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, error) { + se := webrtc.SettingEngine{} + se.DetachDataChannels() + se.SetICETimeouts(time.Second*5, time.Second*5, time.Second*2) + api := webrtc.NewAPI(webrtc.WithSettingEngine(se)) + + return api.NewPeerConnection(webrtc.Configuration{ + ICEServers: servers, + }) +} + +// waitForDataChannelOpen waits for the data channel to have the open state. +// By default, it waits 15 seconds. +func waitForDataChannelOpen(ctx context.Context, channel *webrtc.DataChannel) error { + if channel.ReadyState() == webrtc.DataChannelStateOpen { + return nil + } + ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) + defer cancelFunc() + channel.OnOpen(func() { + cancelFunc() + }) + <-ctx.Done() + if ctx.Err() == context.DeadlineExceeded { + return ctx.Err() + } + return nil +} + +// newProxyDataChannel creates a new data channel for proxying. +func newProxyDataChannel(conn *webrtc.PeerConnection, protocol string, addr string) (*webrtc.DataChannel, error) { + proto := fmt.Sprintf("%s:%s", protocol, addr) + ordered := true + return conn.CreateDataChannel(proto, &webrtc.DataChannelInit{ + Protocol: &proto, + Ordered: &ordered, + }) +} + +// newControlDataChannel creates a new data channel for starting a new peer connection. +func newControlDataChannel(conn *webrtc.PeerConnection) (*webrtc.DataChannel, error) { + proto := "control" + ordered := true + return conn.CreateDataChannel(proto, &webrtc.DataChannelInit{ + Protocol: &proto, + Ordered: &ordered, + }) +} diff --git a/xwebrtc/conn.go b/xwebrtc/conn.go new file mode 100644 index 00000000..0311da98 --- /dev/null +++ b/xwebrtc/conn.go @@ -0,0 +1,80 @@ +package xwebrtc + +import ( + "context" + "io" + "net" + "time" + + "github.com/pion/webrtc/v3" + "golang.org/x/xerrors" +) + +// Conn is a net.Conn based on a data channel. +type Conn struct { + channel *webrtc.DataChannel + rwc io.ReadWriteCloser +} + +// NewConn creates a new data channel on the peer connection and returns it as a net.Conn. +func NewConn(ctx context.Context, rtc *webrtc.PeerConnection, network string, addr string) (net.Conn, error) { + channel, err := newProxyDataChannel(rtc, network, addr) + if err != nil { + return nil, xerrors.Errorf("creating data channel: %w", err) + } + err = waitForDataChannelOpen(ctx, channel) + if err != nil { + return nil, xerrors.Errorf("waiting for open data channel: %w", err) + } + + rwc, err := channel.Detach() + if err != nil { + return nil, xerrors.Errorf("detaching data channel: %w", err) + } + + return &Conn{ + channel: channel, + rwc: rwc, + }, nil +} + +// Read reads data from the connection. +func (c *Conn) Read(b []byte) (n int, err error) { + return c.rwc.Read(b) +} + +// Write writes data to the connection. +func (c *Conn) Write(b []byte) (n int, err error) { + return c.rwc.Write(b) +} + +// Close closes the connection. +// Any blocked Read or Write operations will be unblocked and return errors. +func (c *Conn) Close() error { + return c.rwc.Close() +} + +// LocalAddr is not implemented. +func (c *Conn) LocalAddr() net.Addr { + return nil +} + +// RemoteAddr is not implemented. +func (c *Conn) RemoteAddr() net.Addr { + return nil +} + +// SetDeadline is not implemented. +func (c *Conn) SetDeadline(t time.Time) error { + return nil +} + +// SetReadDeadline is not implemented. +func (c *Conn) SetReadDeadline(t time.Time) error { + return nil +} + +// SetWriteDeadline is not implemented. +func (c *Conn) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/xwebrtc/dialer.go b/xwebrtc/dialer.go new file mode 100644 index 00000000..24bf9b98 --- /dev/null +++ b/xwebrtc/dialer.go @@ -0,0 +1,195 @@ +package xwebrtc + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/url" + + "golang.org/x/net/proxy" + + "cdr.dev/slog" + "github.com/pion/webrtc/v3" + "golang.org/x/xerrors" + "nhooyr.io/websocket" + + "cdr.dev/coder-cli/coder-sdk" + "cdr.dev/coder-cli/pkg/proto" +) + +const ( + // NetworkTCP is the protocol for tcp tunnels. + NetworkTCP = "tcp" +) + +// WorkspaceDialer dials workspace agents and represents peer connections as a http.Client. +type WorkspaceDialer struct { + log slog.Logger + brokerAddr *url.URL + token string + workspaceID string + peerConn *webrtc.PeerConnection +} + +// NewWorkspaceDialer creates a new workspace client to dial to agents. +func NewWorkspaceDialer(ctx context.Context, log slog.Logger, brokerAddr *url.URL, token string, workspaceID string) (proxy.ContextDialer, error) { + client := &WorkspaceDialer{ + log: log, + brokerAddr: brokerAddr, + token: token, + workspaceID: workspaceID, + } + + var err error + client.peerConn, err = client.peerConnection(ctx, workspaceID) + if err != nil { + return nil, xerrors.Errorf("getting peer connection: %w", err) + } + + return client, nil +} + +// DialContext will create a new peer connection with the workspace agent, make a new data channel, and return it as +// a net.Conn. +func (wc *WorkspaceDialer) DialContext(ctx context.Context, network string, workspaceAddr string) (net.Conn, error) { + wc.log.Debug(ctx, "making net conn", slog.F("addr", workspaceAddr)) + nc, err := NewConn(ctx, wc.peerConn, network, workspaceAddr) + if err != nil { + return nil, xerrors.Errorf("creating net conn: %w", err) + } + + return nc, nil +} + +// peerConnection connects to a workspace agent and gives a instantiated connection with the agent. +func (wc *WorkspaceDialer) peerConnection(ctx context.Context, workspaceID string) (*webrtc.PeerConnection, error) { + // Only enabled under a private feature flag for now, + // so insecure connections are entirely fine to allow. + var servers = []webrtc.ICEServer{{ + URLs: []string{turnAddr(wc.brokerAddr)}, + Username: "insecure", + Credential: "pass", + CredentialType: webrtc.ICECredentialTypePassword, + }} + + wc.log.Debug(ctx, "dialing broker", slog.F("url", connnectAddr(wc.brokerAddr, workspaceID, wc.token)), slog.F("servers", servers)) + conn, resp, err := websocket.Dial(ctx, connnectAddr(wc.brokerAddr, workspaceID, wc.token), nil) + if err != nil && resp == nil { + return nil, xerrors.Errorf("dial: %w", err) + } + if err != nil && resp != nil { + defer func() { + _ = resp.Body.Close() + }() + return nil, &coder.HTTPError{ + Response: resp, + } + } + nconn := websocket.NetConn(ctx, conn, websocket.MessageBinary) + defer func() { + _ = nconn.Close() + _ = conn.Close(websocket.StatusNormalClosure, "webrtc handshake complete") + }() + + rtc, err := NewPeerConnection(servers) + if err != nil { + return nil, xerrors.Errorf("create connection: %w", err) + } + + rtc.OnNegotiationNeeded(func() { + wc.log.Debug(ctx, "negotiation needed...") + }) + + rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { + wc.log.Info(ctx, "connection state changed", slog.F("state", pcs)) + }) + + flushCandidates := proto.ProxyICECandidates(rtc, nconn) + + // we make a channel so the handshake actually fires + // but we do nothing with it + control, err := newControlDataChannel(rtc) + if err != nil { + return nil, xerrors.Errorf("create connect data channel: %w", err) + } + go func() { + err = waitForDataChannelOpen(ctx, control) + if err != nil { + wc.log.Fatal(ctx, "waiting for data channel open", slog.Error(err)) + } + _ = control.Close() + _ = conn.Close(websocket.StatusNormalClosure, "rtc connected") + }() + + localDesc, err := rtc.CreateOffer(&webrtc.OfferOptions{}) + if err != nil { + return nil, xerrors.Errorf("create offer: %w", err) + } + + err = rtc.SetLocalDescription(localDesc) + if err != nil { + return nil, xerrors.Errorf("set local desc: %w", err) + } + + b, _ := json.Marshal(&proto.Message{ + Offer: &localDesc, + Servers: servers, + }) + + _, err = nconn.Write(b) + if err != nil { + return nil, xerrors.Errorf("write offer: %w", err) + } + flushCandidates() + + decoder := json.NewDecoder(nconn) + for { + var msg proto.Message + err = decoder.Decode(&msg) + if xerrors.Is(err, io.EOF) { + break + } + if websocket.CloseStatus(err) == websocket.StatusNormalClosure { + break + } + if err != nil { + return nil, xerrors.Errorf("read msg: %w", err) + } + if msg.Candidate != "" { + wc.log.Debug(ctx, "accepted ice candidate", slog.F("candidate", msg.Candidate)) + err = proto.AcceptICECandidate(rtc, &msg) + if err != nil { + return nil, xerrors.Errorf("accept ice: %w", err) + } + continue + } + if msg.Answer != nil { + wc.log.Debug(ctx, "got answer", slog.F("answer", msg.Answer)) + err = rtc.SetRemoteDescription(*msg.Answer) + if err != nil { + return nil, xerrors.Errorf("set remote: %w", err) + } + continue + } + if msg.Error != "" { + return nil, xerrors.Errorf("got error: %s", msg.Error) + } + wc.log.Error(ctx, "unknown message", slog.F("msg", msg)) + } + + return rtc, nil +} + +func turnAddr(u *url.URL) string { + turnScheme := "turns" + if u.Scheme == "http" { + turnScheme = "turn" + } + return fmt.Sprintf("%s:%s:5349?transport=tcp", turnScheme, u.Host) +} + +func connnectAddr(baseURL *url.URL, id string, token string) string { + return fmt.Sprintf("%s%s%s%s%s", baseURL.String(), "/api/private/envagent/", id, "/connect?session_token=", token) +} diff --git a/internal/x/xwebrtc/doc.go b/xwebrtc/doc.go similarity index 100% rename from internal/x/xwebrtc/doc.go rename to xwebrtc/doc.go From 9806f293b74698fea3109444fb57847b79f55e39 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 30 Apr 2021 11:22:12 -0500 Subject: [PATCH 013/128] fix: Properly handle data channel timeout (#328) --- xwebrtc/dialer.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/xwebrtc/dialer.go b/xwebrtc/dialer.go index 24bf9b98..17ace216 100644 --- a/xwebrtc/dialer.go +++ b/xwebrtc/dialer.go @@ -116,10 +116,11 @@ func (wc *WorkspaceDialer) peerConnection(ctx context.Context, workspaceID strin } go func() { err = waitForDataChannelOpen(ctx, control) + _ = control.Close() if err != nil { - wc.log.Fatal(ctx, "waiting for data channel open", slog.Error(err)) + _ = conn.Close(websocket.StatusAbnormalClosure, "data channel timed out") + return } - _ = control.Close() _ = conn.Close(websocket.StatusNormalClosure, "rtc connected") }() From 00667ef637feb39d9d765964ca3272094281ee87 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 3 May 2021 16:49:30 -0500 Subject: [PATCH 014/128] feat: Durability test RTC connections (#329) * Move dial and listen to wsnet * Return proper errors from dial ice * Test sending data * Fix test race * Add disconnect err test * Transfer to use wsnet * Fix xwebrtc reference * Fix comment * Fix go.mod * Fix linting * Fix linting issues * Ignore race conditions for testing the broker * Fix import order * Fix race conditions * Fix trailing whitespace * Ensure proper networking errors get surfaced * Remove agent retry * Fix tunnel command with multiple connections * Reduce API surface --- .gitignore | 1 + agent/doc.go | 2 - agent/server.go | 106 ----------------- agent/stream.go | 194 ------------------------------- go.mod | 6 +- go.sum | 2 - internal/cmd/agent.go | 27 +++-- internal/cmd/tunnel.go | 39 ++++++- pkg/proto/message.go | 62 ---------- wsnet/conn.go | 78 +++++++++++++ wsnet/dial.go | 250 ++++++++++++++++++++++++++++++++++++++++ wsnet/dial_test.go | 153 +++++++++++++++++++++++++ wsnet/doc.go | 3 + wsnet/listen.go | 253 +++++++++++++++++++++++++++++++++++++++++ wsnet/proto.go | 35 ++++++ wsnet/rtc.go | 216 +++++++++++++++++++++++++++++++++++ wsnet/rtc_test.go | 71 ++++++++++++ wsnet/wsnet_test.go | 178 +++++++++++++++++++++++++++++ xwebrtc/channel.go | 76 ------------- xwebrtc/conn.go | 80 ------------- xwebrtc/dialer.go | 196 ------------------------------- xwebrtc/doc.go | 2 - 22 files changed, 1289 insertions(+), 741 deletions(-) delete mode 100644 agent/doc.go delete mode 100644 agent/server.go delete mode 100644 agent/stream.go delete mode 100644 pkg/proto/message.go create mode 100644 wsnet/conn.go create mode 100644 wsnet/dial.go create mode 100644 wsnet/dial_test.go create mode 100644 wsnet/doc.go create mode 100644 wsnet/listen.go create mode 100644 wsnet/proto.go create mode 100644 wsnet/rtc.go create mode 100644 wsnet/rtc_test.go create mode 100644 wsnet/wsnet_test.go delete mode 100644 xwebrtc/channel.go delete mode 100644 xwebrtc/conn.go delete mode 100644 xwebrtc/dialer.go delete mode 100644 xwebrtc/doc.go diff --git a/.gitignore b/.gitignore index c3d31169..11433c18 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ ci/integration/bin ci/integration/env.sh coder-sdk/env.sh .vscode +vendor \ No newline at end of file diff --git a/agent/doc.go b/agent/doc.go deleted file mode 100644 index 46ebd899..00000000 --- a/agent/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package agent is for interacting with p2p server and clients -package agent diff --git a/agent/server.go b/agent/server.go deleted file mode 100644 index 73d399a2..00000000 --- a/agent/server.go +++ /dev/null @@ -1,106 +0,0 @@ -package agent - -import ( - "context" - "fmt" - "net/url" - "time" - - "cdr.dev/slog" - "github.com/hashicorp/yamux" - "go.coder.com/retry" - "golang.org/x/xerrors" - "nhooyr.io/websocket" - - "cdr.dev/coder-cli/coder-sdk" -) - -const ( - listenRoute = "/api/private/envagent/listen" -) - -// Server connects to a Coder deployment and listens for p2p connections. -type Server struct { - log slog.Logger - listenURL *url.URL -} - -// ServerArgs are the required arguments to create an agent server. -type ServerArgs struct { - Log slog.Logger - CoderURL *url.URL - Token string -} - -// NewServer creates a new agent server. -func NewServer(args ServerArgs) (*Server, error) { - lURL, err := formatListenURL(args.CoderURL, args.Token) - if err != nil { - return nil, xerrors.Errorf("formatting listen url: %w", err) - } - - return &Server{ - log: args.Log, - listenURL: lURL, - }, nil -} - -// Run will listen and proxy new peer connections on a retry loop. -func (s *Server) Run(ctx context.Context) error { - err := retry.New(time.Second). - Context(ctx). - Backoff(15 * time.Second). - Conditions( - retry.Condition(func(err error) bool { - if err != nil { - s.log.Error(ctx, "failed to connect", slog.Error(err)) - } - return true - }), - ).Run( - func() error { - s.log.Info(ctx, "connecting to coder", slog.F("url", s.listenURL.String())) - conn, resp, err := websocket.Dial(ctx, s.listenURL.String(), nil) - if err != nil && resp == nil { - return fmt.Errorf("dial: %w", err) - } - if err != nil && resp != nil { - return &coder.HTTPError{ - Response: resp, - } - } - - nc := websocket.NetConn(ctx, conn, websocket.MessageBinary) - session, err := yamux.Server(nc, nil) - if err != nil { - return fmt.Errorf("open: %w", err) - } - s.log.Info(ctx, "connected to coder. awaiting connection requests") - for { - st, err := session.AcceptStream() - if err != nil { - return fmt.Errorf("accept stream: %w", err) - } - stream := &stream{ - logger: s.log.Named(fmt.Sprintf("stream %d", st.StreamID())), - stream: st, - } - go stream.listen() - } - }) - - return err -} - -func formatListenURL(coderURL *url.URL, token string) (*url.URL, error) { - if coderURL.Scheme != "http" && coderURL.Scheme != "https" { - return nil, xerrors.Errorf("invalid URL scheme") - } - - coderURL.Path = listenRoute - q := coderURL.Query() - q.Set("service_token", token) - coderURL.RawQuery = q.Encode() - - return coderURL, nil -} diff --git a/agent/stream.go b/agent/stream.go deleted file mode 100644 index 4a940f5a..00000000 --- a/agent/stream.go +++ /dev/null @@ -1,194 +0,0 @@ -package agent - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net" - - "cdr.dev/coder-cli/xwebrtc" - - "cdr.dev/slog" - "github.com/hashicorp/yamux" - "github.com/pion/webrtc/v3" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/pkg/proto" -) - -type stream struct { - stream *yamux.Stream - logger slog.Logger - - rtc *webrtc.PeerConnection -} - -// writes an error and closes. -func (s *stream) fatal(err error) { - _ = s.write(proto.Message{ - Error: err.Error(), - }) - s.logger.Error(context.Background(), err.Error(), slog.Error(err)) - _ = s.stream.Close() -} - -func (s *stream) listen() { - decoder := json.NewDecoder(s.stream) - for { - var msg proto.Message - err := decoder.Decode(&msg) - if err == io.EOF { - break - } - if err != nil { - s.fatal(err) - return - } - s.processMessage(msg) - } -} - -func (s *stream) write(msg proto.Message) error { - d, err := json.Marshal(&msg) - if err != nil { - return err - } - _, err = s.stream.Write(d) - if err != nil { - return err - } - return nil -} - -func (s *stream) processMessage(msg proto.Message) { - s.logger.Debug(context.Background(), "processing message", slog.F("msg", msg)) - - if msg.Error != "" { - s.fatal(xerrors.New(msg.Error)) - return - } - - if msg.Candidate != "" { - if s.rtc == nil { - s.fatal(xerrors.New("rtc connection must be started before candidates are sent")) - return - } - - s.logger.Debug(context.Background(), "accepted ice candidate", slog.F("candidate", msg.Candidate)) - err := proto.AcceptICECandidate(s.rtc, &msg) - if err != nil { - s.fatal(err) - return - } - } - - if msg.Offer != nil { - if msg.Servers == nil { - s.fatal(fmt.Errorf("servers must be sent with offer")) - return - } - rtc, err := xwebrtc.NewPeerConnection(msg.Servers) - if err != nil { - s.fatal(fmt.Errorf("create connection: %w", err)) - return - } - flushCandidates := proto.ProxyICECandidates(rtc, s.stream) - - err = rtc.SetRemoteDescription(*msg.Offer) - if err != nil { - s.fatal(fmt.Errorf("set remote desc: %w", err)) - return - } - answer, err := rtc.CreateAnswer(nil) - if err != nil { - s.fatal(fmt.Errorf("create answer: %w", err)) - return - } - err = rtc.SetLocalDescription(answer) - if err != nil { - s.fatal(fmt.Errorf("set local desc: %w", err)) - return - } - flushCandidates() - - err = s.write(proto.Message{ - Answer: rtc.LocalDescription(), - }) - if err != nil { - s.fatal(fmt.Errorf("send local desc: %w", err)) - return - } - - rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { - s.logger.Info(context.Background(), "state changed", slog.F("new", pcs)) - }) - rtc.OnDataChannel(s.processDataChannel) - s.rtc = rtc - } -} - -func (s *stream) processDataChannel(channel *webrtc.DataChannel) { - if channel.Protocol() == "control" { - return - } - - if channel.Protocol() == "ping" { - channel.OnOpen(func() { - rw, err := channel.Detach() - if err != nil { - return - } - d := make([]byte, 64) - _, err = rw.Read(d) - if err != nil { - s.logger.Error(context.Background(), "read ping", slog.Error(err)) - return - } - _, err = rw.Write(d) - if err != nil { - s.logger.Error(context.Background(), "write ping", slog.Error(err)) - return - } - }) - return - } - - prto, addr, err := xwebrtc.ParseProxyDataChannel(channel) - if err != nil { - s.fatal(fmt.Errorf("failed to parse proxy data channel: %w", err)) - return - } - if prto != "tcp" { - s.fatal(fmt.Errorf("client provided unsupported protocol: %s", prto)) - return - } - - conn, err := net.Dial(prto, addr) - if err != nil { - s.fatal(fmt.Errorf("failed to dial client addr: %s", addr)) - return - } - - channel.OnOpen(func() { - s.logger.Debug(context.Background(), "proxying data channel", slog.F("addr", addr)) - rw, err := channel.Detach() - if err != nil { - _ = channel.Close() - s.logger.Error(context.Background(), "detach client data channel", slog.Error(err)) - return - } - go func() { - _, err = io.Copy(rw, conn) - if err != nil { - s.logger.Error(context.Background(), "copy to conn", slog.Error(err)) - } - }() - go func() { - _, _ = io.Copy(conn, rw) - if err != nil { - s.logger.Error(context.Background(), "copy from conn", slog.Error(err)) - } - }() - }) -} diff --git a/go.mod b/go.mod index d3129a91..384ea62a 100644 --- a/go.mod +++ b/go.mod @@ -13,12 +13,14 @@ require ( github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f github.com/klauspost/compress v1.10.8 // indirect github.com/manifoldco/promptui v0.8.0 + github.com/pion/datachannel v1.4.21 + github.com/pion/dtls/v2 v2.0.9 + github.com/pion/ice/v2 v2.1.5 + github.com/pion/turn/v2 v2.0.5 github.com/pion/webrtc/v3 v3.0.24 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.1.3 - go.coder.com/retry v1.2.0 - golang.org/x/net v0.0.0-20210420210106-798c2154c571 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 diff --git a/go.sum b/go.sum index 2252f265..4927a840 100644 --- a/go.sum +++ b/go.sum @@ -361,8 +361,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec go.coder.com/cli v0.4.0/go.mod h1:hRTOURCR3LJF1FRW9arecgrzX+AHG7mfYMwThPIgq+w= go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512 h1:DjCS6dRQh+1PlfiBmnabxfdrzenb0tAwJqFxDEH/s9g= go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512/go.mod h1:83JsYgXYv0EOaXjIMnaZ1Fl6ddNB3fJnDZ/8845mUJ8= -go.coder.com/retry v1.2.0 h1:ODdUPu9cb9pcbeAM5j2YqJHUgfFbN60vmhtlWIKZGLo= -go.coder.com/retry v1.2.0/go.mod h1:ihkJszQk8F+yaFL2pcIku9MzbYo+U8vka4IsvQSXVfE= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go index 84995bbb..eef86f05 100644 --- a/internal/cmd/agent.go +++ b/internal/cmd/agent.go @@ -2,15 +2,16 @@ package cmd import ( "context" + "log" "net/url" "os" + "os/signal" + "syscall" - "cdr.dev/slog" - "cdr.dev/slog/sloggers/sloghuman" "github.com/spf13/cobra" "golang.org/x/xerrors" - "cdr.dev/coder-cli/agent" + "cdr.dev/coder-cli/wsnet" ) func agentCmd() *cobra.Command { @@ -46,8 +47,6 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx `, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - log := slog.Make(sloghuman.Sink(cmd.OutOrStdout())) - if coderURL == "" { var ok bool coderURL, ok = os.LookupEnv("CODER_URL") @@ -74,18 +73,18 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx } } - server, err := agent.NewServer(agent.ServerArgs{ - Log: log, - CoderURL: u, - Token: token, - }) + listener, err := wsnet.Listen(context.Background(), wsnet.ListenEndpoint(u, token)) if err != nil { - return xerrors.Errorf("creating agent server: %w", err) + return xerrors.Errorf("listen: %w", err) } - err = server.Run(ctx) - if err != nil && !xerrors.Is(err, context.Canceled) && !xerrors.Is(err, context.DeadlineExceeded) { - return xerrors.Errorf("running agent server: %w", err) + // Block until user sends SIGINT or SIGTERM + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + <-sigs + + if err = listener.Close(); err != nil { + log.Panic(err) } return nil diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index ec8e179d..44f98a96 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "errors" "fmt" "io" "net" @@ -11,12 +12,13 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" + "github.com/pion/webrtc/v3" "github.com/spf13/cobra" "golang.org/x/xerrors" "cdr.dev/coder-cli/coder-sdk" "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/xwebrtc" + "cdr.dev/coder-cli/wsnet" ) func tunnelCmd() *cobra.Command { @@ -102,14 +104,34 @@ type tunnneler struct { } func (c *tunnneler) start(ctx context.Context) error { - wd, err := xwebrtc.NewWorkspaceDialer(ctx, c.log, c.brokerAddr, c.token, c.workspaceID) + server := webrtc.ICEServer{ + URLs: []string{wsnet.TURNEndpoint(c.brokerAddr)}, + Username: "insecure", + Credential: "pass", + CredentialType: webrtc.ICECredentialTypePassword, + } + + err := wsnet.DialICE(server, 0) + if errors.Is(err, wsnet.ErrInvalidCredentials) { + return xerrors.Errorf("failed to authenticate your user for this workspace") + } + if errors.Is(err, wsnet.ErrMismatchedProtocol) { + return xerrors.Errorf("your TURN server is configured incorrectly. check TLS settings") + } if err != nil { - return xerrors.Errorf("creating workspace dialer: %w", wd) + return xerrors.Errorf("dial ice: %w", err) } - nc, err := wd.DialContext(ctx, xwebrtc.NetworkTCP, fmt.Sprintf("localhost:%d", c.remotePort)) + + c.log.Info(ctx, "Connecting to workspace...") + wd, err := wsnet.Dial(ctx, wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token), []webrtc.ICEServer{server}) if err != nil { - return xerrors.Errorf("dial: %w", err) + return xerrors.Errorf("creating workspace dialer: %w", err) } + nc, err := wd.DialContext(ctx, "tcp", fmt.Sprintf("localhost:%d", c.remotePort)) + if err != nil { + return err + } + c.log.Info(ctx, "Connected to workspace!") // proxy via stdio if c.stdio { @@ -122,6 +144,9 @@ func (c *tunnneler) start(ctx context.Context) error { } return nil } + // This was used to test if the port was open, and proxy over stdio + // if the user specified that. + _ = nc.Close() // proxy via tcp listener listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", c.localPort)) @@ -134,6 +159,10 @@ func (c *tunnneler) start(ctx context.Context) error { if err != nil { return xerrors.Errorf("accept: %w", err) } + nc, err := wd.DialContext(ctx, "tcp", fmt.Sprintf("localhost:%d", c.remotePort)) + if err != nil { + return err + } go func() { defer func() { _ = lc.Close() diff --git a/pkg/proto/message.go b/pkg/proto/message.go deleted file mode 100644 index 55f5d91f..00000000 --- a/pkg/proto/message.go +++ /dev/null @@ -1,62 +0,0 @@ -package proto - -import ( - "encoding/json" - "io" - "net/http" - - "github.com/pion/webrtc/v3" -) - -// Message is a common format for agent and client to use in handshake. -type Message struct { - Error string `json:"error"` - Candidate string `json:"candidate"` - Offer *webrtc.SessionDescription `json:"offer"` - Servers []webrtc.ICEServer `json:"servers"` - Answer *webrtc.SessionDescription `json:"answer"` -} - -// WriteError responds with an error and status code. -func WriteError(w http.ResponseWriter, status int, err error) { - w.WriteHeader(status) - _, _ = w.Write([]byte(err.Error())) -} - -// ProxyICECandidates sends all ICE candidates using the message protocol -// to the writer provided. -func ProxyICECandidates(conn *webrtc.PeerConnection, w io.Writer) func() { - queue := make([]*webrtc.ICECandidate, 0) - flushed := false - write := func(i *webrtc.ICECandidate) { - b, _ := json.Marshal(&Message{ - Candidate: i.ToJSON().Candidate, - }) - _, _ = w.Write(b) - } - - conn.OnICECandidate(func(i *webrtc.ICECandidate) { - if i == nil { - return - } - if !flushed { - queue = append(queue, i) - return - } - - write(i) - }) - return func() { - for _, i := range queue { - write(i) - } - flushed = true - } -} - -// AcceptICECandidate adds the candidate to the connection. -func AcceptICECandidate(conn *webrtc.PeerConnection, m *Message) error { - return conn.AddICECandidate(webrtc.ICECandidateInit{ - Candidate: m.Candidate, - }) -} diff --git a/wsnet/conn.go b/wsnet/conn.go new file mode 100644 index 00000000..35d3aff2 --- /dev/null +++ b/wsnet/conn.go @@ -0,0 +1,78 @@ +package wsnet + +import ( + "fmt" + "net" + "net/url" + "time" + + "github.com/pion/datachannel" +) + +const ( + httpScheme = "http" +) + +// TURNEndpoint returns the TURN address for a Coder baseURL. +func TURNEndpoint(baseURL *url.URL) string { + turnScheme := "turns" + if baseURL.Scheme == httpScheme { + turnScheme = "turn" + } + return fmt.Sprintf("%s:%s:5349?transport=tcp", turnScheme, baseURL.Host) +} + +// ListenEndpoint returns the Coder endpoint to listen for workspace connections. +func ListenEndpoint(baseURL *url.URL, token string) string { + wsScheme := "wss" + if baseURL.Scheme == httpScheme { + wsScheme = "ws" + } + return fmt.Sprintf("%s://%s%s?service_token=%s", wsScheme, baseURL.Host, "/api/private/envagent/listen", token) +} + +// ConnectEndpoint returns the Coder endpoint to dial a connection for a workspace. +func ConnectEndpoint(baseURL *url.URL, workspace, token string) string { + wsScheme := "wss" + if baseURL.Scheme == httpScheme { + wsScheme = "ws" + } + return fmt.Sprintf("%s://%s%s%s%s%s", wsScheme, baseURL.Host, "/api/private/envagent/", workspace, "/connect?session_token=", token) +} + +type conn struct { + addr *net.UnixAddr + rw datachannel.ReadWriteCloser +} + +func (c *conn) Read(b []byte) (n int, err error) { + return c.rw.Read(b) +} + +func (c *conn) Write(b []byte) (n int, err error) { + return c.rw.Write(b) +} + +func (c *conn) Close() error { + return c.rw.Close() +} + +func (c *conn) LocalAddr() net.Addr { + return c.addr +} + +func (c *conn) RemoteAddr() net.Addr { + return c.addr +} + +func (c *conn) SetDeadline(t time.Time) error { + return nil +} + +func (c *conn) SetReadDeadline(t time.Time) error { + return nil +} + +func (c *conn) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/wsnet/dial.go b/wsnet/dial.go new file mode 100644 index 00000000..ce92390f --- /dev/null +++ b/wsnet/dial.go @@ -0,0 +1,250 @@ +package wsnet + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "time" + + "github.com/pion/datachannel" + "github.com/pion/webrtc/v3" + "nhooyr.io/websocket" + + "cdr.dev/coder-cli/coder-sdk" +) + +// Dial connects to the broker and negotiates a connection to a listener. +func Dial(ctx context.Context, broker string, iceServers []webrtc.ICEServer) (*Dialer, error) { + if iceServers == nil { + iceServers = []webrtc.ICEServer{} + } + + conn, resp, err := websocket.Dial(ctx, broker, nil) + if err != nil { + if resp != nil { + defer func() { + _ = resp.Body.Close() + }() + return nil, &coder.HTTPError{ + Response: resp, + } + } + return nil, fmt.Errorf("dial websocket: %w", err) + } + nconn := websocket.NetConn(ctx, conn, websocket.MessageBinary) + defer func() { + _ = nconn.Close() + // We should close the socket intentionally. + _ = conn.Close(websocket.StatusInternalError, "an error occurred") + }() + + rtc, err := newPeerConnection(iceServers) + if err != nil { + return nil, fmt.Errorf("create peer connection: %w", err) + } + + flushCandidates := proxyICECandidates(rtc, nconn) + + ctrl, err := rtc.CreateDataChannel(controlChannel, &webrtc.DataChannelInit{ + Protocol: stringPtr(controlChannel), + Ordered: boolPtr(true), + }) + if err != nil { + return nil, fmt.Errorf("create control channel: %w", err) + } + + offer, err := rtc.CreateOffer(&webrtc.OfferOptions{}) + if err != nil { + return nil, fmt.Errorf("create offer: %w", err) + } + err = rtc.SetLocalDescription(offer) + if err != nil { + return nil, fmt.Errorf("set local offer: %w", err) + } + + offerMessage, err := json.Marshal(&protoMessage{ + Offer: &offer, + Servers: iceServers, + }) + if err != nil { + return nil, fmt.Errorf("marshal offer message: %w", err) + } + _, err = nconn.Write(offerMessage) + if err != nil { + return nil, fmt.Errorf("write offer: %w", err) + } + flushCandidates() + + dialer := &Dialer{ + ws: conn, + ctrl: ctrl, + rtc: rtc, + } + + return dialer, dialer.negotiate(nconn) +} + +// Dialer enables arbitrary dialing to any network and address +// inside a workspace. The opposing end of the WebSocket messages +// should be proxied with a Listener. +type Dialer struct { + ws *websocket.Conn + ctrl *webrtc.DataChannel + ctrlrw datachannel.ReadWriteCloser + rtc *webrtc.PeerConnection +} + +func (d *Dialer) negotiate(nconn net.Conn) (err error) { + var ( + decoder = json.NewDecoder(nconn) + errCh = make(chan error) + // If candidates are sent before an offer, we place them here. + // We currently have no assurances to ensure this can't happen, + // so it's better to buffer and process than fail. + pendingCandidates = []webrtc.ICECandidateInit{} + ) + + go func() { + defer close(errCh) + err := waitForDataChannelOpen(context.Background(), d.ctrl) + if err != nil { + _ = d.ws.Close(websocket.StatusAbnormalClosure, "timeout") + errCh <- err + return + } + d.ctrlrw, err = d.ctrl.Detach() + if err != nil { + errCh <- err + } + _ = d.ws.Close(websocket.StatusNormalClosure, "connected") + }() + + for { + var msg protoMessage + err = decoder.Decode(&msg) + if errors.Is(err, io.EOF) { + break + } + if websocket.CloseStatus(err) == websocket.StatusNormalClosure { + // The listener closed the socket because success! + break + } + if err != nil { + return fmt.Errorf("read: %w", err) + } + if msg.Candidate != "" { + c := webrtc.ICECandidateInit{ + Candidate: msg.Candidate, + } + if d.rtc.RemoteDescription() == nil { + pendingCandidates = append(pendingCandidates, c) + continue + } + err = d.rtc.AddICECandidate(c) + if err != nil { + return fmt.Errorf("accept ice candidate: %s: %w", msg.Candidate, err) + } + continue + } + if msg.Answer != nil { + err = d.rtc.SetRemoteDescription(*msg.Answer) + if err != nil { + return fmt.Errorf("set answer: %w", err) + } + for _, candidate := range pendingCandidates { + err = d.rtc.AddICECandidate(candidate) + if err != nil { + return fmt.Errorf("accept pending ice candidate: %s: %w", candidate.Candidate, err) + } + } + pendingCandidates = nil + continue + } + if msg.Error != "" { + return errors.New(msg.Error) + } + return fmt.Errorf("unhandled message: %+v", msg) + } + return <-errCh +} + +// Close closes the RTC connection. +// All data channels dialed will be closed. +func (d *Dialer) Close() error { + return d.rtc.Close() +} + +// Ping sends a ping through the control channel. +func (d *Dialer) Ping(ctx context.Context) error { + _, err := d.ctrlrw.Write([]byte{'a'}) + if err != nil { + return fmt.Errorf("write: %w", err) + } + b := make([]byte, 4) + _, err = d.ctrlrw.Read(b) + return err +} + +// DialContext dials the network and address on the remote listener. +func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + dc, err := d.rtc.CreateDataChannel("proxy", &webrtc.DataChannelInit{ + Ordered: boolPtr(network != "udp"), + Protocol: stringPtr(fmt.Sprintf("%s:%s", network, address)), + }) + if err != nil { + return nil, fmt.Errorf("create data channel: %w", err) + } + err = waitForDataChannelOpen(ctx, dc) + if err != nil { + return nil, fmt.Errorf("wait for open: %w", err) + } + rw, err := dc.Detach() + if err != nil { + return nil, fmt.Errorf("detach: %w", err) + } + + errCh := make(chan error) + go func() { + var init dialChannelMessage + err = json.NewDecoder(rw).Decode(&init) + if err != nil { + errCh <- fmt.Errorf("read init: %w", err) + return + } + if init.Err == "" { + close(errCh) + return + } + err := errors.New(init.Err) + if init.Net != "" { + errCh <- &net.OpError{ + Op: init.Op, + Net: init.Net, + Err: err, + } + return + } + errCh <- err + }() + ctx, cancel := context.WithTimeout(ctx, time.Second*5) + defer cancel() + select { + case err := <-errCh: + if err != nil { + return nil, err + } + case <-ctx.Done(): + return nil, ctx.Err() + } + + return &conn{ + addr: &net.UnixAddr{ + Name: address, + Net: network, + }, + rw: rw, + }, nil +} diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go new file mode 100644 index 00000000..b46bb13f --- /dev/null +++ b/wsnet/dial_test.go @@ -0,0 +1,153 @@ +package wsnet + +import ( + "bytes" + "context" + "errors" + "io" + "net" + "testing" + + "github.com/pion/webrtc/v3" +) + +func ExampleDial_basic() { + servers := []webrtc.ICEServer{{ + URLs: []string{"turns:master.cdr.dev"}, + Username: "kyle", + Credential: "pass", + CredentialType: webrtc.ICECredentialTypePassword, + }} + + for _, server := range servers { + err := DialICE(server, 0) + if errors.Is(err, ErrInvalidCredentials) { + // You could do something... + } + if errors.Is(err, ErrMismatchedProtocol) { + // Likely they used TURNS when they should have used TURN. + // Or they could have used TURN instead of TURNS. + } + } + + dialer, err := Dial(context.Background(), "wss://master.cdr.dev/agent/workspace/connect", servers) + if err != nil { + // Do something... + } + conn, err := dialer.DialContext(context.Background(), "tcp", "localhost:13337") + if err != nil { + // Something... + } + defer conn.Close() + // You now have access to the proxied remote port in `conn`. +} + +func TestDial(t *testing.T) { + t.Run("Ping", func(t *testing.T) { + connectAddr, listenAddr := createDumbBroker(t) + _, err := Listen(context.Background(), listenAddr) + if err != nil { + t.Error(err) + } + dialer, err := Dial(context.Background(), connectAddr, nil) + if err != nil { + t.Error(err) + } + err = dialer.Ping(context.Background()) + if err != nil { + t.Error(err) + } + }) + + t.Run("OPError", func(t *testing.T) { + connectAddr, listenAddr := createDumbBroker(t) + _, err := Listen(context.Background(), listenAddr) + if err != nil { + t.Error(err) + } + dialer, err := Dial(context.Background(), connectAddr, nil) + if err != nil { + t.Error(err) + } + _, err = dialer.DialContext(context.Background(), "tcp", "localhost:100") + if err == nil { + t.Error("should have gotten err") + return + } + _, ok := err.(*net.OpError) + if !ok { + t.Error("invalid error type returned") + return + } + }) + + t.Run("Proxy", func(t *testing.T) { + listener, err := net.Listen("tcp", "0.0.0.0:0") + if err != nil { + t.Error(err) + return + } + msg := []byte("Hello!") + go func() { + conn, err := listener.Accept() + if err != nil { + t.Error(err) + } + _, _ = conn.Write(msg) + }() + + connectAddr, listenAddr := createDumbBroker(t) + _, err = Listen(context.Background(), listenAddr) + if err != nil { + t.Error(err) + } + dialer, err := Dial(context.Background(), connectAddr, nil) + if err != nil { + t.Error(err) + } + conn, err := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) + if err != nil { + t.Error(err) + return + } + rec := make([]byte, len(msg)) + _, err = conn.Read(rec) + if err != nil { + t.Error(err) + return + } + if !bytes.Equal(msg, rec) { + t.Error("bytes were different", string(msg), string(rec)) + } + }) + + // Expect that we'd get an EOF on the server closing. + t.Run("EOF on Close", func(t *testing.T) { + listener, err := net.Listen("tcp", "0.0.0.0:0") + if err != nil { + t.Error(err) + return + } + connectAddr, listenAddr := createDumbBroker(t) + srv, err := Listen(context.Background(), listenAddr) + if err != nil { + t.Error(err) + } + dialer, err := Dial(context.Background(), connectAddr, nil) + if err != nil { + t.Error(err) + } + conn, err := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) + if err != nil { + t.Error(err) + return + } + go srv.Close() + rec := make([]byte, 16) + _, err = conn.Read(rec) + if !errors.Is(err, io.EOF) { + t.Error(err) + return + } + }) +} diff --git a/wsnet/doc.go b/wsnet/doc.go new file mode 100644 index 00000000..3cbdc3ce --- /dev/null +++ b/wsnet/doc.go @@ -0,0 +1,3 @@ +// Package wsnet handles client and server ends of Workspace networking +// negotiations and protocol. +package wsnet diff --git a/wsnet/listen.go b/wsnet/listen.go new file mode 100644 index 00000000..6ce569b4 --- /dev/null +++ b/wsnet/listen.go @@ -0,0 +1,253 @@ +package wsnet + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "strings" + "sync" + + "github.com/hashicorp/yamux" + "github.com/pion/webrtc/v3" + "nhooyr.io/websocket" + + "cdr.dev/coder-cli/coder-sdk" +) + +// Listen connects to the broker proxies connections to the local net. +// Close will end all RTC connections. +func Listen(ctx context.Context, broker string) (io.Closer, error) { + conn, resp, err := websocket.Dial(ctx, broker, nil) + if err != nil { + if resp != nil { + return nil, &coder.HTTPError{ + Response: resp, + } + } + return nil, err + } + nconn := websocket.NetConn(ctx, conn, websocket.MessageBinary) + session, err := yamux.Server(nconn, nil) + if err != nil { + return nil, fmt.Errorf("create multiplex: %w", err) + } + l := &listener{ + ws: conn, + connClosers: make([]io.Closer, 0), + } + go func() { + for { + conn, err := session.Accept() + if err != nil { + if errors.Is(err, io.EOF) { + continue + } + l.acceptError = err + l.Close() + return + } + go l.negotiate(conn) + } + }() + return l, nil +} + +type listener struct { + acceptError error + ws *websocket.Conn + connClosers []io.Closer + connClosersMut sync.Mutex +} + +// Negotiates the handshake protocol over the connection provided. +func (l *listener) negotiate(conn net.Conn) { + var ( + err error + decoder = json.NewDecoder(conn) + rtc *webrtc.PeerConnection + // If candidates are sent before an offer, we place them here. + // We currently have no assurances to ensure this can't happen, + // so it's better to buffer and process than fail. + pendingCandidates = []webrtc.ICECandidateInit{} + // Sends the error provided then closes the connection. + // If RTC isn't connected, we'll close it. + closeError = func(err error) { + d, _ := json.Marshal(&protoMessage{ + Error: err.Error(), + }) + _, _ = conn.Write(d) + _ = conn.Close() + if rtc != nil { + if rtc.ConnectionState() != webrtc.PeerConnectionStateConnected { + rtc.Close() + rtc = nil + } + } + } + ) + + for { + var msg protoMessage + err = decoder.Decode(&msg) + if err != nil { + closeError(err) + return + } + + if msg.Candidate != "" { + c := webrtc.ICECandidateInit{ + Candidate: msg.Candidate, + } + + if rtc == nil { + pendingCandidates = append(pendingCandidates, c) + continue + } + + err = rtc.AddICECandidate(c) + if err != nil { + closeError(fmt.Errorf("accept ice candidate: %w", err)) + return + } + } + + if msg.Offer != nil { + if msg.Servers == nil { + closeError(fmt.Errorf("ICEServers must be provided")) + return + } + rtc, err = newPeerConnection(msg.Servers) + if err != nil { + closeError(err) + return + } + l.connClosersMut.Lock() + l.connClosers = append(l.connClosers, rtc) + l.connClosersMut.Unlock() + rtc.OnDataChannel(l.handle) + flushCandidates := proxyICECandidates(rtc, conn) + err = rtc.SetRemoteDescription(*msg.Offer) + if err != nil { + closeError(fmt.Errorf("apply offer: %w", err)) + return + } + answer, err := rtc.CreateAnswer(nil) + if err != nil { + closeError(fmt.Errorf("create answer: %w", err)) + return + } + err = rtc.SetLocalDescription(answer) + if err != nil { + closeError(fmt.Errorf("set local answer: %w", err)) + return + } + flushCandidates() + + data, err := json.Marshal(&protoMessage{ + Answer: rtc.LocalDescription(), + }) + if err != nil { + closeError(fmt.Errorf("marshal: %w", err)) + return + } + _, err = conn.Write(data) + if err != nil { + closeError(fmt.Errorf("write: %w", err)) + return + } + + for _, candidate := range pendingCandidates { + err = rtc.AddICECandidate(candidate) + if err != nil { + closeError(fmt.Errorf("add pending candidate: %w", err)) + return + } + } + pendingCandidates = nil + } + } +} + +func (l *listener) handle(dc *webrtc.DataChannel) { + if dc.Protocol() == controlChannel { + // The control channel handles pings. + dc.OnOpen(func() { + rw, err := dc.Detach() + if err != nil { + return + } + // We'll read and write back a single byte for ping/pongin'. + d := make([]byte, 1) + for { + _, err = rw.Read(d) + if err != nil { + continue + } + _, _ = rw.Write(d) + } + }) + return + } + + dc.OnOpen(func() { + rw, err := dc.Detach() + if err != nil { + return + } + parts := strings.SplitN(dc.Protocol(), ":", 2) + network := parts[0] + addr := parts[1] + + var init dialChannelMessage + conn, err := net.Dial(network, addr) + if err != nil { + init.Err = err.Error() + if op, ok := err.(*net.OpError); ok { + init.Net = op.Net + init.Op = op.Op + } + } + initData, err := json.Marshal(&init) + if err != nil { + rw.Close() + return + } + _, err = rw.Write(initData) + if err != nil { + return + } + if init.Err != "" { + // If an error occurred, we're safe to close the connection. + dc.Close() + return + } + defer conn.Close() + defer dc.Close() + + go func() { + _, _ = io.Copy(rw, conn) + }() + _, _ = io.Copy(conn, rw) + }) +} + +// Close closes the broker socket and all created RTC connections. +func (l *listener) Close() error { + l.connClosersMut.Lock() + for _, connCloser := range l.connClosers { + // We can ignore the error here... it doesn't + // really matter if these fail to close. + _ = connCloser.Close() + } + l.connClosersMut.Unlock() + return l.ws.Close(websocket.StatusNormalClosure, "") +} + +// Since this listener is bound to the WebSocket, we could +// return that resolved Addr, but until we need it we won't. +func (l *listener) Addr() net.Addr { + return nil +} diff --git a/wsnet/proto.go b/wsnet/proto.go new file mode 100644 index 00000000..cbe3ac82 --- /dev/null +++ b/wsnet/proto.go @@ -0,0 +1,35 @@ +package wsnet + +import ( + "github.com/pion/webrtc/v3" +) + +// protoMessage is used for brokering a dialer and listener. +// +// Dialers initiate an exchange by providing an Offer, +// along with a list of ICE servers for the listener to +// peer with. +// +// The listener should respond with an offer, then both +// sides can begin exchanging candidates. +type protoMessage struct { + // Dialer -> Listener + Offer *webrtc.SessionDescription `json:"offer"` + Servers []webrtc.ICEServer `json:"servers"` + + // Listener -> Dialer + Error string `json:"error"` + Answer *webrtc.SessionDescription `json:"answer"` + + // Bidirectional + Candidate string `json:"candidate"` +} + +// dialChannelMessage is used to notify a dial channel of a +// listening state. Modeled after net.OpError, and marshalled +// to that if Net is not "". +type dialChannelMessage struct { + Err string + Net string + Op string +} diff --git a/wsnet/rtc.go b/wsnet/rtc.go new file mode 100644 index 00000000..4b8a462b --- /dev/null +++ b/wsnet/rtc.go @@ -0,0 +1,216 @@ +package wsnet + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "strings" + "sync" + "time" + + "github.com/pion/dtls/v2" + "github.com/pion/ice/v2" + "github.com/pion/turn/v2" + "github.com/pion/webrtc/v3" +) + +var ( + // ErrMismatchedProtocol occurs when a TURN is requested to a STUN server, + // or a TURN server is requested instead of TURNS. + ErrMismatchedProtocol = errors.New("mismatched protocols") + // ErrInvalidCredentials occurs when invalid credentials are passed to a + // TURN server. This error cannot occur for STUN servers, as they don't accept + // credentials. + ErrInvalidCredentials = errors.New("invalid credentials") + + // Constant for the control channel protocol. + controlChannel = "control" +) + +// DialICE confirms ICE servers are dialable. +// Timeout defaults to 200ms. +func DialICE(server webrtc.ICEServer, timeout time.Duration) error { + for _, rawURL := range server.URLs { + err := dialICEURL(server, rawURL, timeout) + if err != nil { + return err + } + } + return nil +} + +func dialICEURL(server webrtc.ICEServer, rawURL string, timeout time.Duration) error { + url, err := ice.ParseURL(rawURL) + if err != nil { + return err + } + var ( + tcpConn net.Conn + udpConn net.PacketConn + turnServerAddr = fmt.Sprintf("%s:%d", url.Host, url.Port) + ) + switch { + case url.Scheme == ice.SchemeTypeTURN || url.Scheme == ice.SchemeTypeSTUN: + switch url.Proto { + case ice.ProtoTypeUDP: + udpConn, err = net.ListenPacket("udp4", "0.0.0.0:0") + case ice.ProtoTypeTCP: + tcpConn, err = net.Dial("tcp4", turnServerAddr) + } + case url.Scheme == ice.SchemeTypeTURNS || url.Scheme == ice.SchemeTypeSTUNS: + switch url.Proto { + case ice.ProtoTypeUDP: + udpAddr, resErr := net.ResolveUDPAddr("udp4", turnServerAddr) + if resErr != nil { + return resErr + } + dconn, dialErr := dtls.Dial("udp4", udpAddr, &dtls.Config{ + InsecureSkipVerify: true, + }) + err = dialErr + udpConn = turn.NewSTUNConn(dconn) + case ice.ProtoTypeTCP: + tcpConn, err = tls.Dial("tcp4", turnServerAddr, &tls.Config{ + InsecureSkipVerify: true, + }) + } + } + + if err != nil { + return err + } + if tcpConn != nil { + udpConn = turn.NewSTUNConn(tcpConn) + } + defer udpConn.Close() + + var pass string + if server.Credential != nil && server.CredentialType == webrtc.ICECredentialTypePassword { + pass = server.Credential.(string) + } + + client, err := turn.NewClient(&turn.ClientConfig{ + STUNServerAddr: turnServerAddr, + TURNServerAddr: turnServerAddr, + Username: server.Username, + Password: pass, + Realm: "", + Conn: udpConn, + RTO: timeout, + }) + if err != nil { + return err + } + defer client.Close() + err = client.Listen() + if err != nil { + return err + } + // STUN servers are not authenticated with credentials. + // As long as the transport is valid, this should always work. + _, err = client.SendBindingRequest() + if err != nil { + // Transport failed to connect. + // https://github.com/pion/turn/blob/8231b69046f562420299916e9fb69cbff4754231/errors.go#L20 + if strings.Contains(err.Error(), "retransmissions failed") { + return ErrMismatchedProtocol + } + return fmt.Errorf("binding: %w", err) + } + if url.Scheme == ice.SchemeTypeTURN || url.Scheme == ice.SchemeTypeTURNS { + // We TURN to validate server credentials are correct. + pc, err := client.Allocate() + if err != nil { + if strings.Contains(err.Error(), "error 400") { + return ErrInvalidCredentials + } + // Since TURN and STUN follow the same protocol, they can + // both handshake, but once a tunnel is allocated it will + // fail to transmit. + if strings.Contains(err.Error(), "retransmissions failed") { + return ErrMismatchedProtocol + } + return err + } + defer pc.Close() + } + return nil +} + +// Generalizes creating a new peer connection with consistent options. +func newPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, error) { + se := webrtc.SettingEngine{} + se.DetachDataChannels() + se.SetICETimeouts(time.Second*5, time.Second*5, time.Second*2) + api := webrtc.NewAPI(webrtc.WithSettingEngine(se)) + + return api.NewPeerConnection(webrtc.Configuration{ + ICEServers: servers, + }) +} + +// Proxies ICE candidates using the protocol to a writer. +func proxyICECandidates(conn *webrtc.PeerConnection, w io.Writer) func() { + var ( + mut sync.Mutex + queue = []*webrtc.ICECandidate{} + flushed = false + write = func(i *webrtc.ICECandidate) { + b, _ := json.Marshal(&protoMessage{ + Candidate: i.ToJSON().Candidate, + }) + _, _ = w.Write(b) + } + ) + + conn.OnICECandidate(func(i *webrtc.ICECandidate) { + if i == nil { + return + } + if !flushed { + mut.Lock() + defer mut.Unlock() + queue = append(queue, i) + return + } + + write(i) + }) + return func() { + mut.Lock() + defer mut.Unlock() + for _, i := range queue { + write(i) + } + flushed = true + } +} + +// Waits for a DataChannel to hit the open state. +func waitForDataChannelOpen(ctx context.Context, channel *webrtc.DataChannel) error { + if channel.ReadyState() == webrtc.DataChannelStateOpen { + return nil + } + ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) + defer cancelFunc() + channel.OnOpen(func() { + cancelFunc() + }) + <-ctx.Done() + if ctx.Err() == context.DeadlineExceeded { + return ctx.Err() + } + return nil +} + +func stringPtr(s string) *string { + return &s +} + +func boolPtr(b bool) *bool { + return &b +} diff --git a/wsnet/rtc_test.go b/wsnet/rtc_test.go new file mode 100644 index 00000000..c928bef3 --- /dev/null +++ b/wsnet/rtc_test.go @@ -0,0 +1,71 @@ +package wsnet + +import ( + "errors" + "fmt" + "testing" + "time" + + "github.com/pion/ice/v2" + "github.com/pion/webrtc/v3" +) + +func TestDialICE(t *testing.T) { + t.Parallel() + + t.Run("TURN with TLS", func(t *testing.T) { + t.Parallel() + + addr := createTURNServer(t, ice.SchemeTypeTURNS, "test") + err := DialICE(webrtc.ICEServer{ + URLs: []string{fmt.Sprintf("turns:%s", addr)}, + Username: "example", + Credential: "test", + CredentialType: webrtc.ICECredentialTypePassword, + }, time.Millisecond) + if err != nil { + t.Error(err) + } + }) + + t.Run("Protocol mismatch", func(t *testing.T) { + t.Parallel() + + addr := createTURNServer(t, ice.SchemeTypeTURNS, "test") + err := DialICE(webrtc.ICEServer{ + URLs: []string{fmt.Sprintf("turn:%s", addr)}, + Username: "example", + Credential: "test", + CredentialType: webrtc.ICECredentialTypePassword, + }, time.Millisecond) + if !errors.Is(err, ErrMismatchedProtocol) { + t.Error(err) + } + }) + + t.Run("Invalid auth", func(t *testing.T) { + t.Parallel() + + addr := createTURNServer(t, ice.SchemeTypeTURNS, "test") + err := DialICE(webrtc.ICEServer{ + URLs: []string{fmt.Sprintf("turns:%s", addr)}, + Username: "example", + Credential: "invalid", + CredentialType: webrtc.ICECredentialTypePassword, + }, time.Millisecond) + if !errors.Is(err, ErrInvalidCredentials) { + t.Error(err) + } + }) + + t.Run("Protocol mismatch public", func(t *testing.T) { + t.Parallel() + + err := DialICE(webrtc.ICEServer{ + URLs: []string{"turn:stun.l.google.com:19302"}, + }, time.Millisecond) + if !errors.Is(err, ErrMismatchedProtocol) { + t.Error(err) + } + }) +} diff --git a/wsnet/wsnet_test.go b/wsnet/wsnet_test.go new file mode 100644 index 00000000..10a3a5e2 --- /dev/null +++ b/wsnet/wsnet_test.go @@ -0,0 +1,178 @@ +package wsnet + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "io" + "math/big" + "net" + "net/http" + "sync" + "testing" + "time" + + "cdr.dev/slog/sloggers/slogtest/assert" + "github.com/hashicorp/yamux" + "github.com/pion/ice/v2" + "github.com/pion/turn/v2" + "nhooyr.io/websocket" +) + +// createDumbBroker proxies sockets between /listen and /connect +// to emulate an authenticated WebSocket pair. +func createDumbBroker(t *testing.T) (connectAddr string, listenAddr string) { + listener, err := net.Listen("tcp4", "127.0.0.1:0") + if err != nil { + t.Error(err) + } + t.Cleanup(func() { + listener.Close() + }) + var ( + mux = http.NewServeMux() + sess *yamux.Session + mut sync.Mutex + ) + mux.HandleFunc("/listen", func(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, nil) + if err != nil { + t.Error(err) + } + nc := websocket.NetConn(context.Background(), c, websocket.MessageBinary) + mut.Lock() + defer mut.Unlock() + sess, err = yamux.Client(nc, nil) + if err != nil { + t.Error(err) + } + }) + mux.HandleFunc("/connect", func(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, nil) + if err != nil { + t.Error(err) + } + nc := websocket.NetConn(context.Background(), c, websocket.MessageBinary) + mut.Lock() + defer mut.Unlock() + oc, err := sess.Open() + if err != nil { + t.Error(err) + } + go func() { + _, _ = io.Copy(nc, oc) + }() + _, _ = io.Copy(oc, nc) + }) + + s := http.Server{ + Handler: mux, + } + go func() { + _ = s.Serve(listener) + }() + return fmt.Sprintf("ws://%s/connect", listener.Addr()), fmt.Sprintf("ws://%s/listen", listener.Addr()) +} + +// createTURNServer allocates a TURN server and returns the address. +func createTURNServer(t *testing.T, server ice.SchemeType, pass string) string { + var ( + listeners []turn.ListenerConfig + pcListeners []turn.PacketConnConfig + relay = &turn.RelayAddressGeneratorStatic{ + RelayAddress: net.ParseIP("127.0.0.1"), + Address: "127.0.0.1", + } + listenAddr net.Addr + ) + url, _ := ice.ParseURL(fmt.Sprintf("%s:localhost", server)) + + switch url.Proto { + case ice.ProtoTypeTCP: + var ( + tcpListener net.Listener + err error + ) + if url.IsSecure() { + tcpListener, err = tls.Listen("tcp4", "127.0.0.1:0", generateTLSConfig(t)) + } else { + tcpListener, err = net.Listen("tcp4", "127.0.0.1:0") + } + if err != nil { + t.Error(err) + } + listenAddr = tcpListener.Addr() + listeners = []turn.ListenerConfig{{ + Listener: tcpListener, + RelayAddressGenerator: relay, + }} + case ice.ProtoTypeUDP: + udpListener, err := net.ListenPacket("udp4", "127.0.0.1:0") + if err != nil { + t.Error(err) + } + listenAddr = udpListener.LocalAddr() + pcListeners = []turn.PacketConnConfig{{ + PacketConn: udpListener, + RelayAddressGenerator: relay, + }} + } + + srv, err := turn.NewServer(turn.ServerConfig{ + PacketConnConfigs: pcListeners, + ListenerConfigs: listeners, + Realm: "coder", + AuthHandler: func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) { + return turn.GenerateAuthKey(username, realm, pass), true + }, + }) + if err != nil { + t.Error(err) + } + t.Cleanup(func() { + for _, l := range listeners { + l.Listener.Close() + } + for _, l := range pcListeners { + l.PacketConn.Close() + } + srv.Close() + }) + + return listenAddr.String() +} + +func generateTLSConfig(t testing.TB) *tls.Config { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + assert.Success(t, "generate key", err) + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Acme Co"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24 * 180), + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + } + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) + assert.Success(t, "create certificate", err) + certBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + assert.Success(t, "marshal private key", err) + keyBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyBytes}) + cert, err := tls.X509KeyPair(certBytes, keyBytes) + assert.Success(t, "convert to key pair", err) + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: true, + } +} diff --git a/xwebrtc/channel.go b/xwebrtc/channel.go deleted file mode 100644 index 35938a1d..00000000 --- a/xwebrtc/channel.go +++ /dev/null @@ -1,76 +0,0 @@ -package xwebrtc - -import ( - "context" - "fmt" - "strings" - "time" - - "golang.org/x/xerrors" - - "github.com/pion/webrtc/v3" -) - -// ParseProxyDataChannel parses a data channel to get the network and addr. -func ParseProxyDataChannel(channel *webrtc.DataChannel) (string, string, error) { - if channel.Protocol() == "" { - return "", "", xerrors.New("data channel is not a proxy") - } - segments := strings.SplitN(channel.Protocol(), ":", 2) - if len(segments) != 2 { - return "", "", xerrors.Errorf("protocol is malformed: %s", channel.Protocol()) - } - - return segments[0], segments[1], nil -} - -// NewPeerConnection creates a new peer connection. -// It uses the Google stun server by default. -func NewPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, error) { - se := webrtc.SettingEngine{} - se.DetachDataChannels() - se.SetICETimeouts(time.Second*5, time.Second*5, time.Second*2) - api := webrtc.NewAPI(webrtc.WithSettingEngine(se)) - - return api.NewPeerConnection(webrtc.Configuration{ - ICEServers: servers, - }) -} - -// waitForDataChannelOpen waits for the data channel to have the open state. -// By default, it waits 15 seconds. -func waitForDataChannelOpen(ctx context.Context, channel *webrtc.DataChannel) error { - if channel.ReadyState() == webrtc.DataChannelStateOpen { - return nil - } - ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) - defer cancelFunc() - channel.OnOpen(func() { - cancelFunc() - }) - <-ctx.Done() - if ctx.Err() == context.DeadlineExceeded { - return ctx.Err() - } - return nil -} - -// newProxyDataChannel creates a new data channel for proxying. -func newProxyDataChannel(conn *webrtc.PeerConnection, protocol string, addr string) (*webrtc.DataChannel, error) { - proto := fmt.Sprintf("%s:%s", protocol, addr) - ordered := true - return conn.CreateDataChannel(proto, &webrtc.DataChannelInit{ - Protocol: &proto, - Ordered: &ordered, - }) -} - -// newControlDataChannel creates a new data channel for starting a new peer connection. -func newControlDataChannel(conn *webrtc.PeerConnection) (*webrtc.DataChannel, error) { - proto := "control" - ordered := true - return conn.CreateDataChannel(proto, &webrtc.DataChannelInit{ - Protocol: &proto, - Ordered: &ordered, - }) -} diff --git a/xwebrtc/conn.go b/xwebrtc/conn.go deleted file mode 100644 index 0311da98..00000000 --- a/xwebrtc/conn.go +++ /dev/null @@ -1,80 +0,0 @@ -package xwebrtc - -import ( - "context" - "io" - "net" - "time" - - "github.com/pion/webrtc/v3" - "golang.org/x/xerrors" -) - -// Conn is a net.Conn based on a data channel. -type Conn struct { - channel *webrtc.DataChannel - rwc io.ReadWriteCloser -} - -// NewConn creates a new data channel on the peer connection and returns it as a net.Conn. -func NewConn(ctx context.Context, rtc *webrtc.PeerConnection, network string, addr string) (net.Conn, error) { - channel, err := newProxyDataChannel(rtc, network, addr) - if err != nil { - return nil, xerrors.Errorf("creating data channel: %w", err) - } - err = waitForDataChannelOpen(ctx, channel) - if err != nil { - return nil, xerrors.Errorf("waiting for open data channel: %w", err) - } - - rwc, err := channel.Detach() - if err != nil { - return nil, xerrors.Errorf("detaching data channel: %w", err) - } - - return &Conn{ - channel: channel, - rwc: rwc, - }, nil -} - -// Read reads data from the connection. -func (c *Conn) Read(b []byte) (n int, err error) { - return c.rwc.Read(b) -} - -// Write writes data to the connection. -func (c *Conn) Write(b []byte) (n int, err error) { - return c.rwc.Write(b) -} - -// Close closes the connection. -// Any blocked Read or Write operations will be unblocked and return errors. -func (c *Conn) Close() error { - return c.rwc.Close() -} - -// LocalAddr is not implemented. -func (c *Conn) LocalAddr() net.Addr { - return nil -} - -// RemoteAddr is not implemented. -func (c *Conn) RemoteAddr() net.Addr { - return nil -} - -// SetDeadline is not implemented. -func (c *Conn) SetDeadline(t time.Time) error { - return nil -} - -// SetReadDeadline is not implemented. -func (c *Conn) SetReadDeadline(t time.Time) error { - return nil -} - -// SetWriteDeadline is not implemented. -func (c *Conn) SetWriteDeadline(t time.Time) error { - return nil -} diff --git a/xwebrtc/dialer.go b/xwebrtc/dialer.go deleted file mode 100644 index 17ace216..00000000 --- a/xwebrtc/dialer.go +++ /dev/null @@ -1,196 +0,0 @@ -package xwebrtc - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net" - "net/url" - - "golang.org/x/net/proxy" - - "cdr.dev/slog" - "github.com/pion/webrtc/v3" - "golang.org/x/xerrors" - "nhooyr.io/websocket" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/proto" -) - -const ( - // NetworkTCP is the protocol for tcp tunnels. - NetworkTCP = "tcp" -) - -// WorkspaceDialer dials workspace agents and represents peer connections as a http.Client. -type WorkspaceDialer struct { - log slog.Logger - brokerAddr *url.URL - token string - workspaceID string - peerConn *webrtc.PeerConnection -} - -// NewWorkspaceDialer creates a new workspace client to dial to agents. -func NewWorkspaceDialer(ctx context.Context, log slog.Logger, brokerAddr *url.URL, token string, workspaceID string) (proxy.ContextDialer, error) { - client := &WorkspaceDialer{ - log: log, - brokerAddr: brokerAddr, - token: token, - workspaceID: workspaceID, - } - - var err error - client.peerConn, err = client.peerConnection(ctx, workspaceID) - if err != nil { - return nil, xerrors.Errorf("getting peer connection: %w", err) - } - - return client, nil -} - -// DialContext will create a new peer connection with the workspace agent, make a new data channel, and return it as -// a net.Conn. -func (wc *WorkspaceDialer) DialContext(ctx context.Context, network string, workspaceAddr string) (net.Conn, error) { - wc.log.Debug(ctx, "making net conn", slog.F("addr", workspaceAddr)) - nc, err := NewConn(ctx, wc.peerConn, network, workspaceAddr) - if err != nil { - return nil, xerrors.Errorf("creating net conn: %w", err) - } - - return nc, nil -} - -// peerConnection connects to a workspace agent and gives a instantiated connection with the agent. -func (wc *WorkspaceDialer) peerConnection(ctx context.Context, workspaceID string) (*webrtc.PeerConnection, error) { - // Only enabled under a private feature flag for now, - // so insecure connections are entirely fine to allow. - var servers = []webrtc.ICEServer{{ - URLs: []string{turnAddr(wc.brokerAddr)}, - Username: "insecure", - Credential: "pass", - CredentialType: webrtc.ICECredentialTypePassword, - }} - - wc.log.Debug(ctx, "dialing broker", slog.F("url", connnectAddr(wc.brokerAddr, workspaceID, wc.token)), slog.F("servers", servers)) - conn, resp, err := websocket.Dial(ctx, connnectAddr(wc.brokerAddr, workspaceID, wc.token), nil) - if err != nil && resp == nil { - return nil, xerrors.Errorf("dial: %w", err) - } - if err != nil && resp != nil { - defer func() { - _ = resp.Body.Close() - }() - return nil, &coder.HTTPError{ - Response: resp, - } - } - nconn := websocket.NetConn(ctx, conn, websocket.MessageBinary) - defer func() { - _ = nconn.Close() - _ = conn.Close(websocket.StatusNormalClosure, "webrtc handshake complete") - }() - - rtc, err := NewPeerConnection(servers) - if err != nil { - return nil, xerrors.Errorf("create connection: %w", err) - } - - rtc.OnNegotiationNeeded(func() { - wc.log.Debug(ctx, "negotiation needed...") - }) - - rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { - wc.log.Info(ctx, "connection state changed", slog.F("state", pcs)) - }) - - flushCandidates := proto.ProxyICECandidates(rtc, nconn) - - // we make a channel so the handshake actually fires - // but we do nothing with it - control, err := newControlDataChannel(rtc) - if err != nil { - return nil, xerrors.Errorf("create connect data channel: %w", err) - } - go func() { - err = waitForDataChannelOpen(ctx, control) - _ = control.Close() - if err != nil { - _ = conn.Close(websocket.StatusAbnormalClosure, "data channel timed out") - return - } - _ = conn.Close(websocket.StatusNormalClosure, "rtc connected") - }() - - localDesc, err := rtc.CreateOffer(&webrtc.OfferOptions{}) - if err != nil { - return nil, xerrors.Errorf("create offer: %w", err) - } - - err = rtc.SetLocalDescription(localDesc) - if err != nil { - return nil, xerrors.Errorf("set local desc: %w", err) - } - - b, _ := json.Marshal(&proto.Message{ - Offer: &localDesc, - Servers: servers, - }) - - _, err = nconn.Write(b) - if err != nil { - return nil, xerrors.Errorf("write offer: %w", err) - } - flushCandidates() - - decoder := json.NewDecoder(nconn) - for { - var msg proto.Message - err = decoder.Decode(&msg) - if xerrors.Is(err, io.EOF) { - break - } - if websocket.CloseStatus(err) == websocket.StatusNormalClosure { - break - } - if err != nil { - return nil, xerrors.Errorf("read msg: %w", err) - } - if msg.Candidate != "" { - wc.log.Debug(ctx, "accepted ice candidate", slog.F("candidate", msg.Candidate)) - err = proto.AcceptICECandidate(rtc, &msg) - if err != nil { - return nil, xerrors.Errorf("accept ice: %w", err) - } - continue - } - if msg.Answer != nil { - wc.log.Debug(ctx, "got answer", slog.F("answer", msg.Answer)) - err = rtc.SetRemoteDescription(*msg.Answer) - if err != nil { - return nil, xerrors.Errorf("set remote: %w", err) - } - continue - } - if msg.Error != "" { - return nil, xerrors.Errorf("got error: %s", msg.Error) - } - wc.log.Error(ctx, "unknown message", slog.F("msg", msg)) - } - - return rtc, nil -} - -func turnAddr(u *url.URL) string { - turnScheme := "turns" - if u.Scheme == "http" { - turnScheme = "turn" - } - return fmt.Sprintf("%s:%s:5349?transport=tcp", turnScheme, u.Host) -} - -func connnectAddr(baseURL *url.URL, id string, token string) string { - return fmt.Sprintf("%s%s%s%s%s", baseURL.String(), "/api/private/envagent/", id, "/connect?session_token=", token) -} diff --git a/xwebrtc/doc.go b/xwebrtc/doc.go deleted file mode 100644 index ba5f4181..00000000 --- a/xwebrtc/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package xwebrtc contains utilities for working with webrtc connections. -package xwebrtc From 2d16529b071959272e76a0958ee9de16a633921a Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 3 May 2021 21:58:21 +0000 Subject: [PATCH 015/128] fix: Network error RTC race on flush --- wsnet/rtc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wsnet/rtc.go b/wsnet/rtc.go index 4b8a462b..76be9c6e 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -171,9 +171,9 @@ func proxyICECandidates(conn *webrtc.PeerConnection, w io.Writer) func() { if i == nil { return } + mut.Lock() + defer mut.Unlock() if !flushed { - mut.Lock() - defer mut.Unlock() queue = append(queue, i) return } From 8d46612c8a4c73d317a0e9b511a6293dcde35555 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 16:58:39 -0500 Subject: [PATCH 016/128] chore: bump github.com/pion/webrtc/v3 from 3.0.24 to 3.0.27 (#327) Bumps [github.com/pion/webrtc/v3](https://github.com/pion/webrtc) from 3.0.24 to 3.0.27. - [Release notes](https://github.com/pion/webrtc/releases) - [Commits](https://github.com/pion/webrtc/compare/v3.0.24...v3.0.27) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 384ea62a..85060b81 100644 --- a/go.mod +++ b/go.mod @@ -15,9 +15,9 @@ require ( github.com/manifoldco/promptui v0.8.0 github.com/pion/datachannel v1.4.21 github.com/pion/dtls/v2 v2.0.9 - github.com/pion/ice/v2 v2.1.5 + github.com/pion/ice/v2 v2.1.7 github.com/pion/turn/v2 v2.0.5 - github.com/pion/webrtc/v3 v3.0.24 + github.com/pion/webrtc/v3 v3.0.27 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.1.3 diff --git a/go.sum b/go.sum index 4927a840..c8ad70b4 100644 --- a/go.sum +++ b/go.sum @@ -260,8 +260,8 @@ github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXm github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= github.com/pion/dtls/v2 v2.0.9 h1:7Ow+V++YSZQMYzggI0P9vLJz/hUFcffsfGMfT/Qy+u8= github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQKYQho= -github.com/pion/ice/v2 v2.1.5 h1:OqWkKmiLqDU6j+oZnoaxTtO2tR/2QwrUfj3l7BgltzE= -github.com/pion/ice/v2 v2.1.5/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= +github.com/pion/ice/v2 v2.1.7 h1:FjgDfUNrVYTxQabJrkBX6ld12tvYbgzHenqPh3PJF6E= +github.com/pion/ice/v2 v2.1.7/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= github.com/pion/interceptor v0.0.12 h1:eC1iVneBIAQJEfaNAfDqAncJWhMDAnaXPRCJsltdokE= github.com/pion/interceptor v0.0.12/go.mod h1:qzeuWuD/ZXvPqOnxNcnhWfkCZ2e1kwwslicyyPnhoK4= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= @@ -291,8 +291,8 @@ github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= -github.com/pion/webrtc/v3 v3.0.24 h1:IfWd+W7RwISNxK06HolobsXO+qs61xOBzca2cq+RlkE= -github.com/pion/webrtc/v3 v3.0.24/go.mod h1:Qx9zd4xvIeFTN1hygyJ77XVi/YbElyjVitL6KyCEIpE= +github.com/pion/webrtc/v3 v3.0.27 h1:cPQEFNFrRSMT11j9c9aTmXzL3ikKAFPE2kR0ZrQcviw= +github.com/pion/webrtc/v3 v3.0.27/go.mod h1:QpLDmsU5a/a05n230gRtxZRvfHhFzn9ukGUL2x4G5ic= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= From fa33bde260810b20d27b11b10026094bc2113de9 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 4 May 2021 10:05:13 -0500 Subject: [PATCH 017/128] fix: Add TLS option to DialICE (#330) * fix: Add TLS option to DialICE * Fix DialICE timeout arg --- internal/cmd/tunnel.go | 2 +- wsnet/dial_test.go | 2 +- wsnet/rtc.go | 23 +++++++++++++++++------ wsnet/rtc_test.go | 20 ++++++++++++++++---- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index 44f98a96..b21819e2 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -111,7 +111,7 @@ func (c *tunnneler) start(ctx context.Context) error { CredentialType: webrtc.ICECredentialTypePassword, } - err := wsnet.DialICE(server, 0) + err := wsnet.DialICE(server, nil) if errors.Is(err, wsnet.ErrInvalidCredentials) { return xerrors.Errorf("failed to authenticate your user for this workspace") } diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index b46bb13f..fab01069 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -20,7 +20,7 @@ func ExampleDial_basic() { }} for _, server := range servers { - err := DialICE(server, 0) + err := DialICE(server, nil) if errors.Is(err, ErrInvalidCredentials) { // You could do something... } diff --git a/wsnet/rtc.go b/wsnet/rtc.go index 76be9c6e..ce70e557 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -31,11 +31,22 @@ var ( controlChannel = "control" ) +// DialICEOptions provides options for dialing an ICE server. +type DialICEOptions struct { + Timeout time.Duration + // Whether to ignore TLS errors. + InsecureSkipVerify bool +} + // DialICE confirms ICE servers are dialable. // Timeout defaults to 200ms. -func DialICE(server webrtc.ICEServer, timeout time.Duration) error { +func DialICE(server webrtc.ICEServer, options *DialICEOptions) error { + if options == nil { + options = &DialICEOptions{} + } + for _, rawURL := range server.URLs { - err := dialICEURL(server, rawURL, timeout) + err := dialICEURL(server, rawURL, options) if err != nil { return err } @@ -43,7 +54,7 @@ func DialICE(server webrtc.ICEServer, timeout time.Duration) error { return nil } -func dialICEURL(server webrtc.ICEServer, rawURL string, timeout time.Duration) error { +func dialICEURL(server webrtc.ICEServer, rawURL string, options *DialICEOptions) error { url, err := ice.ParseURL(rawURL) if err != nil { return err @@ -69,13 +80,13 @@ func dialICEURL(server webrtc.ICEServer, rawURL string, timeout time.Duration) e return resErr } dconn, dialErr := dtls.Dial("udp4", udpAddr, &dtls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: options.InsecureSkipVerify, }) err = dialErr udpConn = turn.NewSTUNConn(dconn) case ice.ProtoTypeTCP: tcpConn, err = tls.Dial("tcp4", turnServerAddr, &tls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: options.InsecureSkipVerify, }) } } @@ -100,7 +111,7 @@ func dialICEURL(server webrtc.ICEServer, rawURL string, timeout time.Duration) e Password: pass, Realm: "", Conn: udpConn, - RTO: timeout, + RTO: options.Timeout, }) if err != nil { return err diff --git a/wsnet/rtc_test.go b/wsnet/rtc_test.go index c928bef3..14bdd846 100644 --- a/wsnet/rtc_test.go +++ b/wsnet/rtc_test.go @@ -22,7 +22,10 @@ func TestDialICE(t *testing.T) { Username: "example", Credential: "test", CredentialType: webrtc.ICECredentialTypePassword, - }, time.Millisecond) + }, &DialICEOptions{ + Timeout: time.Millisecond, + InsecureSkipVerify: true, + }) if err != nil { t.Error(err) } @@ -37,7 +40,10 @@ func TestDialICE(t *testing.T) { Username: "example", Credential: "test", CredentialType: webrtc.ICECredentialTypePassword, - }, time.Millisecond) + }, &DialICEOptions{ + Timeout: time.Millisecond, + InsecureSkipVerify: true, + }) if !errors.Is(err, ErrMismatchedProtocol) { t.Error(err) } @@ -52,7 +58,10 @@ func TestDialICE(t *testing.T) { Username: "example", Credential: "invalid", CredentialType: webrtc.ICECredentialTypePassword, - }, time.Millisecond) + }, &DialICEOptions{ + Timeout: time.Millisecond, + InsecureSkipVerify: true, + }) if !errors.Is(err, ErrInvalidCredentials) { t.Error(err) } @@ -63,7 +72,10 @@ func TestDialICE(t *testing.T) { err := DialICE(webrtc.ICEServer{ URLs: []string{"turn:stun.l.google.com:19302"}, - }, time.Millisecond) + }, &DialICEOptions{ + Timeout: time.Millisecond, + InsecureSkipVerify: true, + }) if !errors.Is(err, ErrMismatchedProtocol) { t.Error(err) } From e140b59d7ad85500a6e0f2fb545d819b9d4fbeeb Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 4 May 2021 10:32:29 -0500 Subject: [PATCH 018/128] feat: Add authentication to TURN (#331) --- internal/cmd/tunnel.go | 10 +++++++--- wsnet/auth.go | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 wsnet/auth.go diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index b21819e2..fc59ddaa 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -104,14 +104,18 @@ type tunnneler struct { } func (c *tunnneler) start(ctx context.Context) error { + username, password, err := wsnet.TURNCredentials(c.token) + if err != nil { + return xerrors.Errorf("failed to parse credentials from token") + } server := webrtc.ICEServer{ URLs: []string{wsnet.TURNEndpoint(c.brokerAddr)}, - Username: "insecure", - Credential: "pass", + Username: username, + Credential: password, CredentialType: webrtc.ICECredentialTypePassword, } - err := wsnet.DialICE(server, nil) + err = wsnet.DialICE(server, nil) if errors.Is(err, wsnet.ErrInvalidCredentials) { return xerrors.Errorf("failed to authenticate your user for this workspace") } diff --git a/wsnet/auth.go b/wsnet/auth.go new file mode 100644 index 00000000..94ffa59d --- /dev/null +++ b/wsnet/auth.go @@ -0,0 +1,21 @@ +package wsnet + +import ( + "crypto/sha256" + "errors" + "strings" +) + +// TURNCredentials returns a username and password pair +// for a Coder token. +func TURNCredentials(token string) (username, password string, err error) { + str := strings.SplitN(token, "-", 2) + if len(str) != 2 { + err = errors.New("invalid token format") + return + } + username = str[0] + hash := sha256.Sum256([]byte(str[1])) + password = string(hash[:]) + return +} From 16ca4a636be2ff3e344937c232c13eec4e3bdcab Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 7 May 2021 13:19:28 -0500 Subject: [PATCH 019/128] feat: Add DialWebsocket func to enable Dial through net.Conn (#335) --- internal/cmd/tunnel.go | 2 +- wsnet/dial.go | 34 +++++++++++++++++++--------------- wsnet/dial_test.go | 10 +++++----- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index fc59ddaa..254e6df9 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -127,7 +127,7 @@ func (c *tunnneler) start(ctx context.Context) error { } c.log.Info(ctx, "Connecting to workspace...") - wd, err := wsnet.Dial(ctx, wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token), []webrtc.ICEServer{server}) + wd, err := wsnet.DialWebsocket(ctx, wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token), []webrtc.ICEServer{server}) if err != nil { return xerrors.Errorf("creating workspace dialer: %w", err) } diff --git a/wsnet/dial.go b/wsnet/dial.go index ce92390f..6af7293a 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -16,12 +16,8 @@ import ( "cdr.dev/coder-cli/coder-sdk" ) -// Dial connects to the broker and negotiates a connection to a listener. -func Dial(ctx context.Context, broker string, iceServers []webrtc.ICEServer) (*Dialer, error) { - if iceServers == nil { - iceServers = []webrtc.ICEServer{} - } - +// DialWebsocket dials the broker with a WebSocket and negotiates a connection. +func DialWebsocket(ctx context.Context, broker string, iceServers []webrtc.ICEServer) (*Dialer, error) { conn, resp, err := websocket.Dial(ctx, broker, nil) if err != nil { if resp != nil { @@ -40,13 +36,21 @@ func Dial(ctx context.Context, broker string, iceServers []webrtc.ICEServer) (*D // We should close the socket intentionally. _ = conn.Close(websocket.StatusInternalError, "an error occurred") }() + return Dial(ctx, nconn, iceServers) +} + +// Dial negotiates a connection to a listener. +func Dial(ctx context.Context, conn net.Conn, iceServers []webrtc.ICEServer) (*Dialer, error) { + if iceServers == nil { + iceServers = []webrtc.ICEServer{} + } rtc, err := newPeerConnection(iceServers) if err != nil { return nil, fmt.Errorf("create peer connection: %w", err) } - flushCandidates := proxyICECandidates(rtc, nconn) + flushCandidates := proxyICECandidates(rtc, conn) ctrl, err := rtc.CreateDataChannel(controlChannel, &webrtc.DataChannelInit{ Protocol: stringPtr(controlChannel), @@ -72,34 +76,34 @@ func Dial(ctx context.Context, broker string, iceServers []webrtc.ICEServer) (*D if err != nil { return nil, fmt.Errorf("marshal offer message: %w", err) } - _, err = nconn.Write(offerMessage) + _, err = conn.Write(offerMessage) if err != nil { return nil, fmt.Errorf("write offer: %w", err) } flushCandidates() dialer := &Dialer{ - ws: conn, + conn: conn, ctrl: ctrl, rtc: rtc, } - return dialer, dialer.negotiate(nconn) + return dialer, dialer.negotiate() } // Dialer enables arbitrary dialing to any network and address // inside a workspace. The opposing end of the WebSocket messages // should be proxied with a Listener. type Dialer struct { - ws *websocket.Conn + conn net.Conn ctrl *webrtc.DataChannel ctrlrw datachannel.ReadWriteCloser rtc *webrtc.PeerConnection } -func (d *Dialer) negotiate(nconn net.Conn) (err error) { +func (d *Dialer) negotiate() (err error) { var ( - decoder = json.NewDecoder(nconn) + decoder = json.NewDecoder(d.conn) errCh = make(chan error) // If candidates are sent before an offer, we place them here. // We currently have no assurances to ensure this can't happen, @@ -111,7 +115,7 @@ func (d *Dialer) negotiate(nconn net.Conn) (err error) { defer close(errCh) err := waitForDataChannelOpen(context.Background(), d.ctrl) if err != nil { - _ = d.ws.Close(websocket.StatusAbnormalClosure, "timeout") + _ = d.conn.Close() errCh <- err return } @@ -119,7 +123,7 @@ func (d *Dialer) negotiate(nconn net.Conn) (err error) { if err != nil { errCh <- err } - _ = d.ws.Close(websocket.StatusNormalClosure, "connected") + _ = d.conn.Close() }() for { diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index fab01069..84466c36 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -30,7 +30,7 @@ func ExampleDial_basic() { } } - dialer, err := Dial(context.Background(), "wss://master.cdr.dev/agent/workspace/connect", servers) + dialer, err := DialWebsocket(context.Background(), "wss://master.cdr.dev/agent/workspace/connect", servers) if err != nil { // Do something... } @@ -49,7 +49,7 @@ func TestDial(t *testing.T) { if err != nil { t.Error(err) } - dialer, err := Dial(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { t.Error(err) } @@ -65,7 +65,7 @@ func TestDial(t *testing.T) { if err != nil { t.Error(err) } - dialer, err := Dial(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { t.Error(err) } @@ -101,7 +101,7 @@ func TestDial(t *testing.T) { if err != nil { t.Error(err) } - dialer, err := Dial(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { t.Error(err) } @@ -133,7 +133,7 @@ func TestDial(t *testing.T) { if err != nil { t.Error(err) } - dialer, err := Dial(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { t.Error(err) } From 45cae5a271ca0f736d7898aa4bd339d7d16771e2 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 9 May 2021 09:31:40 -0500 Subject: [PATCH 020/128] fix: Remove unused context from RTC Dial (#336) * fix: Remove unused context from Dial * Ignore read/write on closed pipe --- wsnet/dial.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/wsnet/dial.go b/wsnet/dial.go index 6af7293a..1e7dd87a 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -36,11 +36,11 @@ func DialWebsocket(ctx context.Context, broker string, iceServers []webrtc.ICESe // We should close the socket intentionally. _ = conn.Close(websocket.StatusInternalError, "an error occurred") }() - return Dial(ctx, nconn, iceServers) + return Dial(nconn, iceServers) } // Dial negotiates a connection to a listener. -func Dial(ctx context.Context, conn net.Conn, iceServers []webrtc.ICEServer) (*Dialer, error) { +func Dial(conn net.Conn, iceServers []webrtc.ICEServer) (*Dialer, error) { if iceServers == nil { iceServers = []webrtc.ICEServer{} } @@ -129,11 +129,7 @@ func (d *Dialer) negotiate() (err error) { for { var msg protoMessage err = decoder.Decode(&msg) - if errors.Is(err, io.EOF) { - break - } - if websocket.CloseStatus(err) == websocket.StatusNormalClosure { - // The listener closed the socket because success! + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe) { break } if err != nil { From 50933f6de2b7adf6e5be1b6fe6bd3b5ebda3b477 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 9 May 2021 11:14:31 -0500 Subject: [PATCH 021/128] fix: Loop Dial for reconnects (#337) * fix: Loop Dial for reconnects * Use base64 for password credential * Fix unused comments * Disable cognit for func * Fix slow TURN dials --- wsnet/auth.go | 3 +- wsnet/dial_test.go | 6 ++++ wsnet/listen.go | 81 +++++++++++++++++++++++++++++++++++----------- wsnet/rtc.go | 12 +++++++ 4 files changed, 82 insertions(+), 20 deletions(-) diff --git a/wsnet/auth.go b/wsnet/auth.go index 94ffa59d..a5daf45e 100644 --- a/wsnet/auth.go +++ b/wsnet/auth.go @@ -2,6 +2,7 @@ package wsnet import ( "crypto/sha256" + "encoding/base64" "errors" "strings" ) @@ -16,6 +17,6 @@ func TURNCredentials(token string) (username, password string, err error) { } username = str[0] hash := sha256.Sum256([]byte(str[1])) - password = string(hash[:]) + password = base64.StdEncoding.EncodeToString(hash[:]) return } diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 84466c36..3d2e1f1c 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -48,10 +48,12 @@ func TestDial(t *testing.T) { _, err := Listen(context.Background(), listenAddr) if err != nil { t.Error(err) + return } dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { t.Error(err) + return } err = dialer.Ping(context.Background()) if err != nil { @@ -64,6 +66,7 @@ func TestDial(t *testing.T) { _, err := Listen(context.Background(), listenAddr) if err != nil { t.Error(err) + return } dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { @@ -100,10 +103,12 @@ func TestDial(t *testing.T) { _, err = Listen(context.Background(), listenAddr) if err != nil { t.Error(err) + return } dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { t.Error(err) + return } conn, err := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) if err != nil { @@ -132,6 +137,7 @@ func TestDial(t *testing.T) { srv, err := Listen(context.Background(), listenAddr) if err != nil { t.Error(err) + return } dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { diff --git a/wsnet/listen.go b/wsnet/listen.go index 6ce569b4..4382f503 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -9,6 +9,7 @@ import ( "net" "strings" "sync" + "time" "github.com/hashicorp/yamux" "github.com/pion/webrtc/v3" @@ -20,7 +21,51 @@ import ( // Listen connects to the broker proxies connections to the local net. // Close will end all RTC connections. func Listen(ctx context.Context, broker string) (io.Closer, error) { - conn, resp, err := websocket.Dial(ctx, broker, nil) + l := &listener{ + broker: broker, + connClosers: make([]io.Closer, 0), + } + // We do a one-off dial outside of the loop to ensure the initial + // connection is successful. If not, there's likely an error the + // user needs to act on. + ch, err := l.dial(ctx) + if err != nil { + return nil, err + } + go func() { + for { + err := <-ch + if errors.Is(err, io.EOF) { + // If we hit an EOF, then the connection to the broker + // was interrupted. We'll take a short break then dial + // again. + time.Sleep(time.Second) + ch, err = l.dial(ctx) + } + if err != nil { + l.acceptError = err + _ = l.Close() + break + } + } + }() + return l, nil +} + +type listener struct { + broker string + + acceptError error + ws *websocket.Conn + connClosers []io.Closer + connClosersMut sync.Mutex +} + +func (l *listener) dial(ctx context.Context) (<-chan error, error) { + if l.ws != nil { + _ = l.ws.Close(websocket.StatusNormalClosure, "new connection inbound") + } + conn, resp, err := websocket.Dial(ctx, l.broker, nil) if err != nil { if resp != nil { return nil, &coder.HTTPError{ @@ -29,40 +74,31 @@ func Listen(ctx context.Context, broker string) (io.Closer, error) { } return nil, err } + l.ws = conn nconn := websocket.NetConn(ctx, conn, websocket.MessageBinary) session, err := yamux.Server(nconn, nil) if err != nil { return nil, fmt.Errorf("create multiplex: %w", err) } - l := &listener{ - ws: conn, - connClosers: make([]io.Closer, 0), - } + errCh := make(chan error) go func() { + defer close(errCh) for { conn, err := session.Accept() if err != nil { - if errors.Is(err, io.EOF) { - continue - } - l.acceptError = err - l.Close() - return + errCh <- err + break } go l.negotiate(conn) } }() - return l, nil -} - -type listener struct { - acceptError error - ws *websocket.Conn - connClosers []io.Closer - connClosersMut sync.Mutex + return errCh, nil } // Negotiates the handshake protocol over the connection provided. +// This functions control-flow is important to readability, +// so the cognitive overload linter has been disabled. +// nolint:gocognit func (l *listener) negotiate(conn net.Conn) { var ( err error @@ -119,6 +155,13 @@ func (l *listener) negotiate(conn net.Conn) { closeError(fmt.Errorf("ICEServers must be provided")) return } + for _, server := range msg.Servers { + err = DialICE(server, nil) + if err != nil { + closeError(fmt.Errorf("dial server %+v: %w", server.URLs, err)) + return + } + } rtc, err = newPeerConnection(msg.Servers) if err != nil { closeError(err) diff --git a/wsnet/rtc.go b/wsnet/rtc.go index ce70e557..bd08baf0 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -157,6 +157,18 @@ func newPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, erro se := webrtc.SettingEngine{} se.DetachDataChannels() se.SetICETimeouts(time.Second*5, time.Second*5, time.Second*2) + + // If one server is provided and we know it's TURN, we can set the + // relay acceptable so the connection starts immediately. + if len(servers) == 1 { + server := servers[0] + if server.Credential != nil && len(server.URLs) == 1 { + url, err := ice.ParseURL(server.URLs[0]) + if err == nil && url.Proto == ice.ProtoTypeTCP { + se.SetRelayAcceptanceMinWait(0) + } + } + } api := webrtc.NewAPI(webrtc.WithSettingEngine(se)) return api.NewPeerConnection(webrtc.Configuration{ From ed0464aa3e87bf09a89b6b07aaae42b352a8f11d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 9 May 2021 11:56:19 -0500 Subject: [PATCH 022/128] chore: bump github.com/pion/webrtc/v3 from 3.0.27 to 3.0.29 (#334) Bumps [github.com/pion/webrtc/v3](https://github.com/pion/webrtc) from 3.0.27 to 3.0.29. - [Release notes](https://github.com/pion/webrtc/releases) - [Commits](https://github.com/pion/webrtc/compare/v3.0.27...v3.0.29) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 85060b81..10e1aca3 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/pion/dtls/v2 v2.0.9 github.com/pion/ice/v2 v2.1.7 github.com/pion/turn/v2 v2.0.5 - github.com/pion/webrtc/v3 v3.0.27 + github.com/pion/webrtc/v3 v3.0.29 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.1.3 diff --git a/go.sum b/go.sum index c8ad70b4..80622413 100644 --- a/go.sum +++ b/go.sum @@ -272,8 +272,9 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo= github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= -github.com/pion/rtp v1.6.2 h1:iGBerLX6JiDjB9NXuaPzHyxHFG9JsIEdgwTC0lp5n/U= github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/rtp v1.6.5 h1:o2cZf8OascA5HF/b0PAbTxRKvOWxTQxWYt7SlToxFGI= +github.com/pion/rtp v1.6.5/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= github.com/pion/sctp v1.7.12 h1:GsatLufywVruXbZZT1CKg+Jr8ZTkwiPnmUC/oO9+uuY= github.com/pion/sctp v1.7.12/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= @@ -291,8 +292,8 @@ github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= -github.com/pion/webrtc/v3 v3.0.27 h1:cPQEFNFrRSMT11j9c9aTmXzL3ikKAFPE2kR0ZrQcviw= -github.com/pion/webrtc/v3 v3.0.27/go.mod h1:QpLDmsU5a/a05n230gRtxZRvfHhFzn9ukGUL2x4G5ic= +github.com/pion/webrtc/v3 v3.0.29 h1:pVs6mYjbbYvC8pMsztayEz35DnUEFLPswsicGXaQjxo= +github.com/pion/webrtc/v3 v3.0.29/go.mod h1:XFQeLYBf++bWWA0sJqh6zF1ouWluosxwTOMOoTZGaD0= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= From bb4ced4d364d89674131676579574447dd73f44c Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 9 May 2021 15:38:29 -0500 Subject: [PATCH 023/128] fix: Stop infinite read loop on control channel (#338) --- wsnet/listen.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wsnet/listen.go b/wsnet/listen.go index 4382f503..55d6d019 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -226,6 +226,9 @@ func (l *listener) handle(dc *webrtc.DataChannel) { d := make([]byte, 1) for { _, err = rw.Read(d) + if errors.Is(err, io.EOF) { + return + } if err != nil { continue } From f2c17d6bda034bb0516497f94ee3c2238594ddc3 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 10 May 2021 15:37:45 -0500 Subject: [PATCH 024/128] fix: Change SSH port for p2p (#339) * fix: Change SSH port for p2p * Fix formatting --- internal/cmd/configssh.go | 7 ++++--- internal/cmd/tunnel.go | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index 120e9a86..0853a32e 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -205,14 +205,15 @@ func makeSSHConfig(host, userName, envName, privateKeyFilepath string, p2p bool) if p2p { return fmt.Sprintf( `Host coder.%s - HostName localhost - ProxyCommand coder tunnel %s 22 stdio + HostName coder.%s + ProxyCommand coder tunnel %s 12213 stdio StrictHostKeyChecking no ConnectTimeout=0 + IdentitiesOnly yes IdentityFile="%s" ServerAliveInterval 60 ServerAliveCountMax 3 -`, envName, envName, privateKeyFilepath) +`, envName, envName, envName, privateKeyFilepath) } return fmt.Sprintf( diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index 254e6df9..6194891a 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -126,7 +126,7 @@ func (c *tunnneler) start(ctx context.Context) error { return xerrors.Errorf("dial ice: %w", err) } - c.log.Info(ctx, "Connecting to workspace...") + c.log.Debug(ctx, "Connecting to workspace...") wd, err := wsnet.DialWebsocket(ctx, wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token), []webrtc.ICEServer{server}) if err != nil { return xerrors.Errorf("creating workspace dialer: %w", err) @@ -135,7 +135,7 @@ func (c *tunnneler) start(ctx context.Context) error { if err != nil { return err } - c.log.Info(ctx, "Connected to workspace!") + c.log.Debug(ctx, "Connected to workspace!") // proxy via stdio if c.stdio { From 08ef03efa6e1f8730bfd9bb97b63312680488f04 Mon Sep 17 00:00:00 2001 From: G r e y Date: Mon, 10 May 2021 16:57:44 -0500 Subject: [PATCH 025/128] fix: Optional --name for urls create [ch8011] (#340) --- coder-sdk/devurl.go | 2 +- docs/coder_urls.md | 2 +- docs/coder_urls_create.md | 10 ++++++++-- internal/cmd/urls.go | 8 +++----- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/coder-sdk/devurl.go b/coder-sdk/devurl.go index ea7e013e..27a18366 100644 --- a/coder-sdk/devurl.go +++ b/coder-sdk/devurl.go @@ -40,7 +40,7 @@ type CreateDevURLReq struct { Scheme string `json:"scheme"` } -// CreateDevURL inserts a new devurl for the authenticated user. +// CreateDevURL inserts a new dev URL for the authenticated user. func (c *DefaultClient) CreateDevURL(ctx context.Context, envID string, req CreateDevURLReq) error { return c.requestBody(ctx, http.MethodPost, "/api/v0/environments/"+envID+"/devurls", req, nil) } diff --git a/docs/coder_urls.md b/docs/coder_urls.md index 39cddf89..efe016d1 100644 --- a/docs/coder_urls.md +++ b/docs/coder_urls.md @@ -17,7 +17,7 @@ Interact with environment DevURLs ### SEE ALSO * [coder](coder.md) - coder provides a CLI for working with an existing Coder installation -* [coder urls create](coder_urls_create.md) - Create a new devurl for an environment +* [coder urls create](coder_urls_create.md) - Create a new dev URL for a workspace * [coder urls ls](coder_urls_ls.md) - List all DevURLs for an environment * [coder urls rm](coder_urls_rm.md) - Remove a dev url diff --git a/docs/coder_urls_create.md b/docs/coder_urls_create.md index ac60824d..81228aaa 100644 --- a/docs/coder_urls_create.md +++ b/docs/coder_urls_create.md @@ -1,9 +1,15 @@ ## coder urls create -Create a new devurl for an environment +Create a new dev URL for a workspace ``` -coder urls create [env_name] [port] [flags] +coder urls create [workspace_name] [port] [flags] +``` + +### Examples + +``` +coder urls create my-workspace 8080 --name my-dev-url ``` ### Options diff --git a/internal/cmd/urls.go b/internal/cmd/urls.go index 94f90357..271073b2 100644 --- a/internal/cmd/urls.go +++ b/internal/cmd/urls.go @@ -122,11 +122,11 @@ func createDevURLCmd() *cobra.Command { scheme string ) cmd := &cobra.Command{ - Use: "create [env_name] [port]", - Short: "Create a new devurl for an environment", + Use: "create [workspace_name] [port]", + Short: "Create a new dev URL for a workspace", Aliases: []string{"edit"}, Args: xcobra.ExactArgs(2), - // Run creates or updates a devURL + Example: `coder urls create my-workspace 8080 --name my-dev-url`, RunE: func(cmd *cobra.Command, args []string) error { var ( envName = args[0] @@ -195,8 +195,6 @@ func createDevURLCmd() *cobra.Command { cmd.Flags().StringVar(&access, "access", "private", "Set DevURL access to [private | org | authed | public]") cmd.Flags().StringVar(&urlname, "name", "", "DevURL name") cmd.Flags().StringVar(&scheme, "scheme", "http", "Server scheme (http|https)") - _ = cmd.MarkFlagRequired("name") - return cmd } From f44c5cab6a83c2842f75dde37e256097beac06d1 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 10 May 2021 22:12:16 -0500 Subject: [PATCH 026/128] chore: Test RTC listener reconnects (#342) * chore: Test RTC listener reconnects * Remove version check for tunnel * Fix linting issues * Update Go version * Pin to 1.16.3 --- .github/workflows/build.yaml | 6 ++++ ci/image/Dockerfile | 2 +- internal/cmd/agent.go | 2 +- internal/cmd/auth.go | 12 ++++--- internal/cmd/configssh.go | 2 +- internal/cmd/envs.go | 12 +++---- internal/cmd/images.go | 2 +- internal/cmd/providers.go | 12 +++---- internal/cmd/rebuild.go | 4 +-- internal/cmd/resourcemanager.go | 2 +- internal/cmd/ssh.go | 4 +-- internal/cmd/sync.go | 2 +- internal/cmd/tags.go | 6 ++-- internal/cmd/tokens.go | 8 ++--- internal/cmd/tunnel.go | 4 +-- internal/cmd/urls.go | 6 ++-- internal/cmd/users.go | 2 +- wsnet/listen.go | 9 +++-- wsnet/listen_test.go | 62 +++++++++++++++++++++++++++++++++ 19 files changed, 118 insertions(+), 41 deletions(-) create mode 100644 wsnet/listen_test.go diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index cb2e37a7..a0d76acf 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -7,6 +7,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v1 + - uses: actions/setup-go@v2 + with: + go-version: '^1.16.3' - name: Build run: make -j build/linux build/windows - name: Upload @@ -19,6 +22,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v1 + - uses: actions/setup-go@v2 + with: + go-version: '^1.16.3' - name: Install Gon run: | brew tap mitchellh/gon diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index f814866d..08dde84b 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1 +FROM golang:1.16.3 ENV GOFLAGS="-mod=readonly" ENV CI=true diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go index eef86f05..38853dd1 100644 --- a/internal/cmd/agent.go +++ b/internal/cmd/agent.go @@ -51,7 +51,7 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx var ok bool coderURL, ok = os.LookupEnv("CODER_URL") if !ok { - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return xerrors.New("must login, pass --coder-url flag, or set the CODER_URL env variable") } diff --git a/internal/cmd/auth.go b/internal/cmd/auth.go index a6e73d16..56aaef80 100644 --- a/internal/cmd/auth.go +++ b/internal/cmd/auth.go @@ -23,7 +23,7 @@ var errNeedLogin = clog.Fatal( const tokenEnv = "CODER_TOKEN" const urlEnv = "CODER_URL" -func newClient(ctx context.Context) (coder.Client, error) { +func newClient(ctx context.Context, checkVersion bool) (coder.Client, error) { var ( err error sessionToken = os.Getenv(tokenEnv) @@ -55,10 +55,14 @@ func newClient(ctx context.Context) (coder.Client, error) { return nil, xerrors.Errorf("failed to create new coder.Client: %w", err) } - apiVersion, err := c.APIVersion(ctx) - if apiVersion != "" && !version.VersionsMatch(apiVersion) { - logVersionMismatchError(apiVersion) + if checkVersion { + var apiVersion string + apiVersion, err = c.APIVersion(ctx) + if apiVersion != "" && !version.VersionsMatch(apiVersion) { + logVersionMismatchError(apiVersion) + } } + if err != nil { var he *coder.HTTPError if xerrors.As(err, &he) { diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index 0853a32e..7abe2c2b 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -88,7 +88,7 @@ func configSSH(configpath *string, remove *bool, p2p *bool) func(cmd *cobra.Comm return nil } - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } diff --git a/internal/cmd/envs.go b/internal/cmd/envs.go index c33db37d..352b2567 100644 --- a/internal/cmd/envs.go +++ b/internal/cmd/envs.go @@ -59,7 +59,7 @@ func lsEnvsCommand() *cobra.Command { Long: "List all Coder environments owned by the active user.", RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -128,7 +128,7 @@ coder envs --user charlie@coder.com ls -o json \ Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return xerrors.Errorf("new client: %w", err) } @@ -189,7 +189,7 @@ coder envs create my-new-powerful-env --cpu 12 --disk 100 --memory 16 --image ub return xerrors.New("image unset") } - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -315,7 +315,7 @@ coder envs create-from-config --name="dev-env" -f coder.yaml`, ) } - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -438,7 +438,7 @@ func editEnvCmd() *cobra.Command { coder envs edit back-end-env --disk 20`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -532,7 +532,7 @@ func rmEnvsCmd() *cobra.Command { Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } diff --git a/internal/cmd/images.go b/internal/cmd/images.go index 70364a59..ccb68ee5 100644 --- a/internal/cmd/images.go +++ b/internal/cmd/images.go @@ -38,7 +38,7 @@ func lsImgsCommand(user *string) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } diff --git a/internal/cmd/providers.go b/internal/cmd/providers.go index 46074933..5081df2b 100644 --- a/internal/cmd/providers.go +++ b/internal/cmd/providers.go @@ -50,7 +50,7 @@ coder providers create my-provider --hostname=https://provider.example.com --clu RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -143,7 +143,7 @@ coder providers ls`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -174,7 +174,7 @@ func deleteProviderCmd() *cobra.Command { coder providers rm my-workspace-provider`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -232,7 +232,7 @@ func cordonProviderCmd() *cobra.Command { coder providers cordon my-workspace-provider --reason "limit cloud clost"`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -265,7 +265,7 @@ func unCordonProviderCmd() *cobra.Command { coder providers uncordon my-workspace-provider`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -296,7 +296,7 @@ func renameProviderCmd() *cobra.Command { coder providers rename build-in us-east-1`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } diff --git a/internal/cmd/rebuild.go b/internal/cmd/rebuild.go index fe6c3dff..4df7d8e9 100644 --- a/internal/cmd/rebuild.go +++ b/internal/cmd/rebuild.go @@ -29,7 +29,7 @@ func rebuildEnvCommand() *cobra.Command { coder envs rebuild backend-env --force`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -166,7 +166,7 @@ func watchBuildLogCommand() *cobra.Command { Args: xcobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } diff --git a/internal/cmd/resourcemanager.go b/internal/cmd/resourcemanager.go index 5611594f..85d39c98 100644 --- a/internal/cmd/resourcemanager.go +++ b/internal/cmd/resourcemanager.go @@ -62,7 +62,7 @@ coder resources top --sort-by memory --show-empty`, func runResourceTop(options *resourceTopOptions) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } diff --git a/internal/cmd/ssh.go b/internal/cmd/ssh.go index f062fa11..34478e0a 100644 --- a/internal/cmd/ssh.go +++ b/internal/cmd/ssh.go @@ -37,7 +37,7 @@ coder ssh my-dev pwd`, func shell(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -99,7 +99,7 @@ func shValidArgs(cmd *cobra.Command, args []string) error { ctx := cmd.Context() err := cobra.MinimumNArgs(1)(cmd, args) if err != nil { - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return clog.Error("missing [environment_name] argument") } diff --git a/internal/cmd/sync.go b/internal/cmd/sync.go index fc059ac0..922f2679 100644 --- a/internal/cmd/sync.go +++ b/internal/cmd/sync.go @@ -54,7 +54,7 @@ func makeRunSync(init *bool) func(cmd *cobra.Command, args []string) error { remote = args[1] ) - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } diff --git a/internal/cmd/tags.go b/internal/cmd/tags.go index 91d7ba19..201133b0 100644 --- a/internal/cmd/tags.go +++ b/internal/cmd/tags.go @@ -41,7 +41,7 @@ func tagsCreateCmd() *cobra.Command { Args: xcobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -86,7 +86,7 @@ func tagsLsCmd() *cobra.Command { Example: `coder tags ls --image ubuntu --org default --output json`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -143,7 +143,7 @@ func tagsRmCmd() *cobra.Command { Args: xcobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } diff --git a/internal/cmd/tokens.go b/internal/cmd/tokens.go index 21fd478f..0763a5b0 100644 --- a/internal/cmd/tokens.go +++ b/internal/cmd/tokens.go @@ -36,7 +36,7 @@ func lsTokensCmd() *cobra.Command { Short: "show the user's active API tokens", RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -79,7 +79,7 @@ func createTokensCmd() *cobra.Command { Args: xcobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -102,7 +102,7 @@ func rmTokenCmd() *cobra.Command { Args: xcobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -121,7 +121,7 @@ func regenTokenCmd() *cobra.Command { Args: xcobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index 6194891a..09463589 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -49,7 +49,7 @@ coder tunnel my-dev 3000 3000 } } - sdk, err := newClient(ctx) + sdk, err := newClient(ctx, false) if err != nil { return xerrors.Errorf("getting coder client: %w", err) } @@ -72,7 +72,7 @@ coder tunnel my-dev 3000 3000 } c := &tunnneler{ - log: log.Leveled(slog.LevelDebug), + log: log, brokerAddr: &baseURL, token: sdk.Token(), workspaceID: envID, diff --git a/internal/cmd/urls.go b/internal/cmd/urls.go index 271073b2..64836229 100644 --- a/internal/cmd/urls.go +++ b/internal/cmd/urls.go @@ -81,7 +81,7 @@ func accessLevelIsValid(level string) bool { func listDevURLsCmd(outputFmt *string) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -147,7 +147,7 @@ func createDevURLCmd() *cobra.Command { if urlname != "" && !devURLNameValidRx.MatchString(urlname) { return xerrors.New("update devurl: name must be < 64 chars in length, begin with a letter and only contain letters or digits.") } - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } @@ -228,7 +228,7 @@ func removeDevURL(cmd *cobra.Command, args []string) error { return xerrors.Errorf("validate port: %w", err) } - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } diff --git a/internal/cmd/users.go b/internal/cmd/users.go index 03929366..c9a00343 100644 --- a/internal/cmd/users.go +++ b/internal/cmd/users.go @@ -32,7 +32,7 @@ coder users ls -o json | jq .[] | jq -r .email`, func listUsers(outputFmt *string) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - client, err := newClient(ctx) + client, err := newClient(ctx, true) if err != nil { return err } diff --git a/wsnet/listen.go b/wsnet/listen.go index 55d6d019..9a9a30db 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -18,6 +18,8 @@ import ( "cdr.dev/coder-cli/coder-sdk" ) +var keepAliveInterval = 5 * time.Second + // Listen connects to the broker proxies connections to the local net. // Close will end all RTC connections. func Listen(ctx context.Context, broker string) (io.Closer, error) { @@ -35,7 +37,7 @@ func Listen(ctx context.Context, broker string) (io.Closer, error) { go func() { for { err := <-ch - if errors.Is(err, io.EOF) { + if errors.Is(err, io.EOF) || errors.Is(err, yamux.ErrKeepAliveTimeout) { // If we hit an EOF, then the connection to the broker // was interrupted. We'll take a short break then dial // again. @@ -76,7 +78,10 @@ func (l *listener) dial(ctx context.Context) (<-chan error, error) { } l.ws = conn nconn := websocket.NetConn(ctx, conn, websocket.MessageBinary) - session, err := yamux.Server(nconn, nil) + config := yamux.DefaultConfig() + config.KeepAliveInterval = keepAliveInterval + config.LogOutput = io.Discard + session, err := yamux.Server(nconn, config) if err != nil { return nil, fmt.Errorf("create multiplex: %w", err) } diff --git a/wsnet/listen_test.go b/wsnet/listen_test.go new file mode 100644 index 00000000..d228bd09 --- /dev/null +++ b/wsnet/listen_test.go @@ -0,0 +1,62 @@ +package wsnet + +import ( + "context" + "fmt" + "net" + "net/http" + "testing" + "time" + + "nhooyr.io/websocket" +) + +func TestListen(t *testing.T) { + t.Run("Reconnect", func(t *testing.T) { + keepAliveInterval = 50 * time.Millisecond + + var ( + connCh = make(chan interface{}) + mux = http.NewServeMux() + srv = http.Server{ + Handler: mux, + } + ) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + _, err := websocket.Accept(w, r, nil) + if err != nil { + t.Error(err) + return + } + connCh <- struct{}{} + }) + + listener, err := net.Listen("tcp4", "127.0.0.1:0") + if err != nil { + t.Error(err) + return + } + go func() { + _ = srv.Serve(listener) + }() + addr := listener.Addr() + broker := fmt.Sprintf("http://%s/", addr.String()) + + _, err = Listen(context.Background(), broker) + if err != nil { + t.Error(err) + return + } + <-connCh + _ = listener.Close() + listener, err = net.Listen("tcp4", addr.String()) + if err != nil { + t.Error(err) + return + } + go func() { + _ = srv.Serve(listener) + }() + <-connCh + }) +} From aa6625c00ee319a92469c14077aed936c52bc9e7 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 10 May 2021 22:52:31 -0500 Subject: [PATCH 027/128] perf: Reduce RTC connection times (#341) --- wsnet/dial.go | 26 +++++++++++++++++++------- wsnet/listen.go | 10 ++++++++-- wsnet/rtc.go | 22 ++++++++++++++++++++++ 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/wsnet/dial.go b/wsnet/dial.go index 1e7dd87a..c8f0f7e3 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -113,17 +113,16 @@ func (d *Dialer) negotiate() (err error) { go func() { defer close(errCh) - err := waitForDataChannelOpen(context.Background(), d.ctrl) + err := waitForConnectionOpen(context.Background(), d.rtc) if err != nil { _ = d.conn.Close() errCh <- err return } - d.ctrlrw, err = d.ctrl.Detach() - if err != nil { - errCh <- err - } - _ = d.conn.Close() + go func() { + // Closing this connection took 30ms+. + _ = d.conn.Close() + }() }() for { @@ -179,7 +178,20 @@ func (d *Dialer) Close() error { // Ping sends a ping through the control channel. func (d *Dialer) Ping(ctx context.Context) error { - _, err := d.ctrlrw.Write([]byte{'a'}) + // Since we control the client and server we could open this + // data channel with `Negotiated` true to reduce traffic being + // sent when the RTC connection is opened. + err := waitForDataChannelOpen(context.Background(), d.ctrl) + if err != nil { + return err + } + if d.ctrlrw == nil { + d.ctrlrw, err = d.ctrl.Detach() + if err != nil { + return err + } + } + _, err = d.ctrlrw.Write([]byte{'a'}) if err != nil { return fmt.Errorf("write: %w", err) } diff --git a/wsnet/listen.go b/wsnet/listen.go index 9a9a30db..5a159e52 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -103,7 +103,7 @@ func (l *listener) dial(ctx context.Context) (<-chan error, error) { // Negotiates the handshake protocol over the connection provided. // This functions control-flow is important to readability, // so the cognitive overload linter has been disabled. -// nolint:gocognit +// nolint:gocognit,nestif func (l *listener) negotiate(conn net.Conn) { var ( err error @@ -172,11 +172,17 @@ func (l *listener) negotiate(conn net.Conn) { closeError(err) return } + rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { + if pcs == webrtc.PeerConnectionStateConnecting { + return + } + _ = conn.Close() + }) + flushCandidates := proxyICECandidates(rtc, conn) l.connClosersMut.Lock() l.connClosers = append(l.connClosers, rtc) l.connClosersMut.Unlock() rtc.OnDataChannel(l.handle) - flushCandidates := proxyICECandidates(rtc, conn) err = rtc.SetRemoteDescription(*msg.Offer) if err != nil { closeError(fmt.Errorf("apply offer: %w", err)) diff --git a/wsnet/rtc.go b/wsnet/rtc.go index bd08baf0..9c07663d 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -155,6 +155,8 @@ func dialICEURL(server webrtc.ICEServer, rawURL string, options *DialICEOptions) // Generalizes creating a new peer connection with consistent options. func newPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, error) { se := webrtc.SettingEngine{} + se.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeUDP4}) + se.SetSrflxAcceptanceMinWait(0) se.DetachDataChannels() se.SetICETimeouts(time.Second*5, time.Second*5, time.Second*2) @@ -165,6 +167,7 @@ func newPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, erro if server.Credential != nil && len(server.URLs) == 1 { url, err := ice.ParseURL(server.URLs[0]) if err == nil && url.Proto == ice.ProtoTypeTCP { + se.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6}) se.SetRelayAcceptanceMinWait(0) } } @@ -213,6 +216,25 @@ func proxyICECandidates(conn *webrtc.PeerConnection, w io.Writer) func() { } } +// Waits for a PeerConnection to hit the open state. +func waitForConnectionOpen(ctx context.Context, conn *webrtc.PeerConnection) error { + if conn.ConnectionState() == webrtc.PeerConnectionStateConnected { + return nil + } + ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) + defer cancelFunc() + conn.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { + if pcs == webrtc.PeerConnectionStateConnected { + cancelFunc() + } + }) + <-ctx.Done() + if ctx.Err() == context.DeadlineExceeded { + return ctx.Err() + } + return nil +} + // Waits for a DataChannel to hit the open state. func waitForDataChannelOpen(ctx context.Context, channel *webrtc.DataChannel) error { if channel.ReadyState() == webrtc.DataChannelStateOpen { From 75ef1bc5aee1d7cc985ab545015ca509ea11fbb0 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 11 May 2021 00:03:14 -0500 Subject: [PATCH 028/128] Use site config to use tunnel on workspaces in built-in wsp and site p2p enabled (#343) --- coder-sdk/config.go | 18 ++++++++++++++++++ coder-sdk/interface.go | 3 +++ docs/coder_config-ssh.md | 1 - internal/cmd/configssh.go | 21 +++++++++++++-------- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/coder-sdk/config.go b/coder-sdk/config.go index 7012f90d..c43ddf2c 100644 --- a/coder-sdk/config.go +++ b/coder-sdk/config.go @@ -137,3 +137,21 @@ func (c *DefaultClient) SiteConfigExtensionMarketplace(ctx context.Context) (*Co func (c *DefaultClient) PutSiteConfigExtensionMarketplace(ctx context.Context, req ConfigExtensionMarketplace) error { return c.requestBody(ctx, http.MethodPut, "/api/private/extensions/config", req, nil) } + +// ConfigWorkspaces is the site configuration for workspace attributes. +type ConfigWorkspaces struct { + GPUVendor string `json:"gpu_vendor,omitempty" valid:"in(nvidia|amd)"` + EnableContainerVMs bool `json:"enable_container_vms,omitempty"` + EnableWorkspacesAsCode bool `json:"enable_workspaces_as_code,omitempty"` + EnableP2P bool `json:"enable_p2p,omitempty"` +} + +// SiteConfigWorkspaces fetches the workspace configuration. +func (c *DefaultClient) SiteConfigWorkspaces(ctx context.Context) (*ConfigWorkspaces, error) { + var conf ConfigWorkspaces + // TODO: use the `/api/v0/workspaces/config route once we migrate from using general config + if err := c.requestBody(ctx, http.MethodGet, "/api/private/config", nil, &conf); err != nil { + return nil, err + } + return &conf, nil +} diff --git a/coder-sdk/interface.go b/coder-sdk/interface.go index ef9995c1..42c34d2c 100644 --- a/coder-sdk/interface.go +++ b/coder-sdk/interface.go @@ -62,6 +62,9 @@ type Client interface { // PutSiteConfigExtensionMarketplace sets the extension marketplace configuration. PutSiteConfigExtensionMarketplace(ctx context.Context, req ConfigExtensionMarketplace) error + // SiteConfigWorkspaces fetches the workspace configuration. + SiteConfigWorkspaces(ctx context.Context) (*ConfigWorkspaces, error) + // DeleteDevURL deletes the specified devurl. DeleteDevURL(ctx context.Context, envID, urlID string) error diff --git a/docs/coder_config-ssh.md b/docs/coder_config-ssh.md index 311387aa..8ac849cf 100644 --- a/docs/coder_config-ssh.md +++ b/docs/coder_config-ssh.md @@ -15,7 +15,6 @@ coder config-ssh [flags] ``` --filepath string override the default path of your ssh config file (default "~/.ssh/config") -h, --help help for config-ssh - --p2p (experimental) uses coder tunnel to proxy ssh connection --remove remove the auto-generated Coder ssh config ``` diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index 7abe2c2b..8314b5a9 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -35,23 +35,21 @@ func configSSHCmd() *cobra.Command { var ( configpath string remove = false - p2p = false ) cmd := &cobra.Command{ Use: "config-ssh", Short: "Configure SSH to access Coder environments", Long: "Inject the proper OpenSSH configuration into your local SSH config file.", - RunE: configSSH(&configpath, &remove, &p2p), + RunE: configSSH(&configpath, &remove), } cmd.Flags().StringVar(&configpath, "filepath", filepath.Join("~", ".ssh", "config"), "override the default path of your ssh config file") cmd.Flags().BoolVar(&remove, "remove", false, "remove the auto-generated Coder ssh config") - cmd.Flags().BoolVar(&p2p, "p2p", false, "(experimental) uses coder tunnel to proxy ssh connection") return cmd } -func configSSH(configpath *string, remove *bool, p2p *bool) func(cmd *cobra.Command, _ []string) error { +func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []string) error { return func(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() usr, err := user.Current() @@ -115,7 +113,12 @@ func configSSH(configpath *string, remove *bool, p2p *bool) func(cmd *cobra.Comm return xerrors.New("SSH is disabled or not available for any environments in your Coder deployment.") } - newConfig := makeNewConfigs(user.Username, envsWithProviders, privateKeyFilepath, *p2p) + wconf, err := client.SiteConfigWorkspaces(ctx) + if err != nil { + return xerrors.Errorf("getting site workspace config: %w", err) + } + + newConfig := makeNewConfigs(user.Username, envsWithProviders, privateKeyFilepath, wconf.EnableP2P) err = os.MkdirAll(filepath.Dir(*configpath), os.ModePerm) if err != nil { @@ -194,15 +197,17 @@ func makeNewConfigs(userName string, envs []coderutil.EnvWithWorkspaceProvider, clog.LogWarn("invalid access url", clog.Causef("malformed url: %q", env.WorkspaceProvider.EnvproxyAccessURL)) continue } - newConfig += makeSSHConfig(u.Host, userName, env.Env.Name, privateKeyFilepath, p2p) + + useTunnel := env.WorkspaceProvider.BuiltIn && p2p + newConfig += makeSSHConfig(u.Host, userName, env.Env.Name, privateKeyFilepath, useTunnel) } newConfig += fmt.Sprintf("\n%s\n", sshEndToken) return newConfig } -func makeSSHConfig(host, userName, envName, privateKeyFilepath string, p2p bool) string { - if p2p { +func makeSSHConfig(host, userName, envName, privateKeyFilepath string, tunnel bool) string { + if tunnel { return fmt.Sprintf( `Host coder.%s HostName coder.%s From 84ab0d70cccbd74d634c79ddcae43a950482e7b5 Mon Sep 17 00:00:00 2001 From: Jonathan Yu Date: Tue, 11 May 2021 14:23:38 -0700 Subject: [PATCH 029/128] chore: update golangci-lint configuration (#344) --- .github/workflows/test.yaml | 14 +++- .golangci.yml | 157 +++++++++++++++++++++++------------- 2 files changed, 115 insertions(+), 56 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a3cf3c60..79a324a9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,36 +6,44 @@ jobs: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 + - uses: actions/cache@v2 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- + - name: fmt uses: ./ci/image with: args: make -j fmt + - run: ./ci/scripts/files_changed.sh + lint: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 + - name: golangci-lint uses: golangci/golangci-lint-action@v2.5.2 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.36 + version: v1.39 + test: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 + - uses: actions/cache@v2 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- + - name: test uses: ./ci/image env: @@ -45,18 +53,22 @@ jobs: CODER_PASSWORD: ${{ secrets.CODER_PASSWORD }} with: args: make -j test/coverage + gendocs: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 + - uses: actions/cache@v2 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- + - name: generate-docs uses: ./ci/image with: args: make -j gendocs + - run: ./ci/scripts/files_changed.sh diff --git a/.golangci.yml b/.golangci.yml index 4bde9808..df85d74b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,73 +1,120 @@ # See https://golangci-lint.run/usage/configuration/ linters-settings: - goconst: - min-len: 4 - min-occurrences: 3 gocognit: # tunnel.go has a 150 line function. Someone should fix it and # decrement this back down to a rational number. min-complexity: 52 + goconst: + min-len: 4 + min-occurrences: 3 nestif: min-complexity: 10 - govet: - settings: - printf: - funcs: # Run `go tool vet help printf` to see available settings for `printf` analyzer. - - (cdr.dev/coder-cli/pkg/clog).Tipf - - (cdr.dev/coder-cli/pkg/clog).Hintf - - (cdr.dev/coder-cli/pkg/clog).Causef + revive: + # see https://github.com/mgechev/revive#available-rules for details. + ignore-generated-header: true + severity: warning + rules: + - name: atomic + # - name: bare-return + - name: blank-imports + - name: bool-literal-in-expr + - name: call-to-gc + - name: confusing-naming + - name: confusing-results + - name: constant-logical-expr + - name: context-as-argument + - name: context-keys-type + # - name: deep-exit + - name: defer + - name: dot-imports + - name: duplicated-imports + # - name: early-return + # - name: empty-block + - name: empty-lines + - name: error-naming + - name: error-return + - name: error-strings + - name: errorf + - name: exported + # - name: flag-parameter + - name: get-return + - name: identical-branches + # - name: if-return + # - name: import-shadowing + - name: increment-decrement + - name: indent-error-flow + - name: modifies-parameter + - name: modifies-value-receiver + - name: package-comments + - name: range + - name: range-val-address + - name: range-val-in-closure + - name: receiver-naming + - name: redefines-builtin-id + - name: string-of-int + - name: struct-tag + - name: superfluous-else + - name: time-naming + - name: unconditional-recursion + - name: unexported-naming + - name: unexported-return + # - name: unhandled-error + - name: unnecessary-stmt + - name: unreachable-code + # - name: unused-parameter + # - name: unused-receiver + # - name: var-declaration + - name: var-naming + - name: waitgroup-by-value + +issues: + fix: true + max-issues-per-linter: 0 + max-same-issues: 0 + +run: + timeout: 5m + linters: disable-all: true - exclude-use-default: false enable: - - megacheck - - govet - - golint - - goconst - - gocognit - - nestif - - misspell - - unparam - - unused - bodyclose - deadcode - - depguard - dogsled - errcheck - - unconvert - - unparam - - varcheck - - whitespace + # - errorlint + - exportloopref + # - forcetypeassert + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - gofmt + - goimports + - golint + - gomodguard + - goprintffuncname + # - gosec + - gosimple + - govet + - ineffassign + - makezero + - megacheck + - misspell + - nestif + - noctx + - nolintlint + - revive + - rowserrcheck + - sqlclosecheck + - staticcheck - structcheck - stylecheck - typecheck - - nolintlint - - rowserrcheck - - scopelint - - goprintffuncname - - gofmt - - godot - - ineffassign - - gocritic - -issues: - exclude-use-default: false - exclude: - # errcheck: Almost all programs ignore errors on these functions and in most cases it's ok - - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked - # golint: False positive when tests are defined in package 'test' - - func name will be used as test\.Test.* by other packages, and that stutters; consider calling this - # govet: Common false positives - - (possible misuse of unsafe.Pointer|should have signature) - # staticcheck: Developers tend to write in C-style with an explicit 'break' in a 'switch', so it's ok to ignore - - ineffective break statement. Did you mean to break out of the outer loop - # gosec: Too many false-positives on 'unsafe' usage - - Use of unsafe calls should be audited - # gosec: Too many false-positives for parametrized shell calls - - Subprocess launch(ed with variable|ing should be audited) - # gosec: Duplicated errcheck checks - - G104 - # gosec: Too many issues in popular repos - - (Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less) - # gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)' - - Potential file inclusion via variable + - unconvert + - unparam + - unused + - varcheck + - wastedassign + - whitespace From 99b3008bcb2add548230b93de201842c7c33ca23 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 11 May 2021 19:07:24 -0500 Subject: [PATCH 030/128] fix: TestDial/EOF_on_Close listen race (#345) --- wsnet/dial_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 3d2e1f1c..584c37d5 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -133,6 +133,9 @@ func TestDial(t *testing.T) { t.Error(err) return } + go func() { + _, _ = listener.Accept() + }() connectAddr, listenAddr := createDumbBroker(t) srv, err := Listen(context.Background(), listenAddr) if err != nil { From 0021226d64cc098fa08e3527eabab5b9cc43674f Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 12 May 2021 12:33:58 -0500 Subject: [PATCH 031/128] Add --next flag to config-ssh (#347) --- docs/coder_config-ssh.md | 1 + internal/cmd/configssh.go | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/coder_config-ssh.md b/docs/coder_config-ssh.md index 8ac849cf..7aba039f 100644 --- a/docs/coder_config-ssh.md +++ b/docs/coder_config-ssh.md @@ -15,6 +15,7 @@ coder config-ssh [flags] ``` --filepath string override the default path of your ssh config file (default "~/.ssh/config") -h, --help help for config-ssh + --next (alpha) uses coder tunnel to proxy ssh connection --remove remove the auto-generated Coder ssh config ``` diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index 8314b5a9..ee493660 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -35,21 +35,23 @@ func configSSHCmd() *cobra.Command { var ( configpath string remove = false + next = false ) cmd := &cobra.Command{ Use: "config-ssh", Short: "Configure SSH to access Coder environments", Long: "Inject the proper OpenSSH configuration into your local SSH config file.", - RunE: configSSH(&configpath, &remove), + RunE: configSSH(&configpath, &remove, &next), } cmd.Flags().StringVar(&configpath, "filepath", filepath.Join("~", ".ssh", "config"), "override the default path of your ssh config file") cmd.Flags().BoolVar(&remove, "remove", false, "remove the auto-generated Coder ssh config") + cmd.Flags().BoolVar(&next, "next", false, "(alpha) uses coder tunnel to proxy ssh connection") return cmd } -func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []string) error { +func configSSH(configpath *string, remove *bool, next *bool) func(cmd *cobra.Command, _ []string) error { return func(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() usr, err := user.Current() @@ -117,8 +119,20 @@ func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []st if err != nil { return xerrors.Errorf("getting site workspace config: %w", err) } + p2p := false + if wconf.EnableP2P { + if *next { + p2p = true + } else { + fmt.Println("Note: NetworkingV2 is enabled on the coder deployment, use --next to enable it for ssh") + } + } else { + if *next { + return xerrors.New("NetworkingV2 feature is not enabled, cannot use --next flag") + } + } - newConfig := makeNewConfigs(user.Username, envsWithProviders, privateKeyFilepath, wconf.EnableP2P) + newConfig := makeNewConfigs(user.Username, envsWithProviders, privateKeyFilepath, p2p) err = os.MkdirAll(filepath.Dir(*configpath), os.ModePerm) if err != nil { From 9ddbd855349e8a77d0d31370ea300ba301647725 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 12 May 2021 13:13:33 -0500 Subject: [PATCH 032/128] fix: Retry RTC connection attempts multiple times (#348) * fix: Retry RTC connection attempts multiple times * Fix test race with retryInterval --- wsnet/listen.go | 18 ++++++++++++++---- wsnet/listen_test.go | 22 ++++++++++++++++------ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/wsnet/listen.go b/wsnet/listen.go index 5a159e52..859ba17f 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -18,7 +18,7 @@ import ( "cdr.dev/coder-cli/coder-sdk" ) -var keepAliveInterval = 5 * time.Second +var connectionRetryInterval = time.Second // Listen connects to the broker proxies connections to the local net. // Close will end all RTC connections. @@ -41,8 +41,19 @@ func Listen(ctx context.Context, broker string) (io.Closer, error) { // If we hit an EOF, then the connection to the broker // was interrupted. We'll take a short break then dial // again. - time.Sleep(time.Second) - ch, err = l.dial(ctx) + ticker := time.NewTicker(connectionRetryInterval) + for { + select { + case <-ticker.C: + ch, err = l.dial(ctx) + case <-ctx.Done(): + err = ctx.Err() + } + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + break + } + } + ticker.Stop() } if err != nil { l.acceptError = err @@ -79,7 +90,6 @@ func (l *listener) dial(ctx context.Context) (<-chan error, error) { l.ws = conn nconn := websocket.NetConn(ctx, conn, websocket.MessageBinary) config := yamux.DefaultConfig() - config.KeepAliveInterval = keepAliveInterval config.LogOutput = io.Discard session, err := yamux.Server(nconn, config) if err != nil { diff --git a/wsnet/listen_test.go b/wsnet/listen_test.go index d228bd09..45519b92 100644 --- a/wsnet/listen_test.go +++ b/wsnet/listen_test.go @@ -11,24 +11,27 @@ import ( "nhooyr.io/websocket" ) +func init() { + // We override this value to make tests faster. + connectionRetryInterval = 10 * time.Millisecond +} + func TestListen(t *testing.T) { t.Run("Reconnect", func(t *testing.T) { - keepAliveInterval = 50 * time.Millisecond - var ( - connCh = make(chan interface{}) + connCh = make(chan *websocket.Conn) mux = http.NewServeMux() srv = http.Server{ Handler: mux, } ) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - _, err := websocket.Accept(w, r, nil) + ws, err := websocket.Accept(w, r, nil) if err != nil { t.Error(err) return } - connCh <- struct{}{} + connCh <- ws }) listener, err := net.Listen("tcp4", "127.0.0.1:0") @@ -47,8 +50,15 @@ func TestListen(t *testing.T) { t.Error(err) return } - <-connCh + conn := <-connCh _ = listener.Close() + // We need to close the connection too... closing a TCP + // listener does not close active local connections. + _ = conn.Close(websocket.StatusGoingAway, "") + + // At least a few retry attempts should be had... + time.Sleep(connectionRetryInterval * 5) + listener, err = net.Listen("tcp4", addr.String()) if err != nil { t.Error(err) From d8e5efedf62fccdc202978f19975594c82e07433 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 12 May 2021 22:02:22 -0500 Subject: [PATCH 033/128] fix: Stop looping retries after dial failure (#349) * fix: Stop looping retries after dial failure * Merge if statements Co-authored-by: Dean Sheather Co-authored-by: Dean Sheather --- wsnet/listen.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wsnet/listen.go b/wsnet/listen.go index 859ba17f..c11df79c 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -49,7 +49,7 @@ func Listen(ctx context.Context, broker string) (io.Closer, error) { case <-ctx.Done(): err = ctx.Err() } - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { break } } From 56ffab3ffc2b1138fcc426a053019e5ecc14198b Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 14 May 2021 13:16:19 -0500 Subject: [PATCH 034/128] Rename environments to workspaces and use new routes (#350) * Rename env types to workspace * Rename envs sub-command to workspaces Aliases are `envs` and `workspaces`. * Add a deprecated envs command There doesn't appear to be a way to deprecate an alias so create a new command instead. --- ci/integration/integration_test.go | 8 +- ci/integration/ssh_test.go | 12 +- ci/integration/statictokens_test.go | 6 +- coder-sdk/activity.go | 10 +- coder-sdk/activity_test.go | 8 +- coder-sdk/devurl.go | 36 +- coder-sdk/interface.go | 84 ++--- coder-sdk/org.go | 2 +- coder-sdk/tags.go | 16 +- coder-sdk/{env.go => workspace.go} | 209 +++++------ coder-sdk/workspace_providers.go | 2 +- docs/coder.md | 10 +- docs/coder_completion.md | 2 +- docs/coder_config-ssh.md | 2 +- docs/coder_envs.md | 32 -- docs/coder_envs_create-from-config.md | 43 --- docs/coder_envs_create.md | 47 --- docs/coder_envs_edit.md | 46 --- docs/coder_envs_rm.md | 26 -- docs/coder_envs_stop.md | 44 --- docs/coder_ssh.md | 4 +- docs/coder_sync.md | 4 +- docs/coder_tokens.md | 2 +- docs/coder_urls.md | 4 +- docs/coder_urls_create.md | 2 +- docs/coder_urls_ls.md | 6 +- docs/coder_urls_rm.md | 4 +- docs/coder_workspaces.md | 32 ++ docs/coder_workspaces_create-from-config.md | 43 +++ docs/coder_workspaces_create.md | 47 +++ docs/coder_workspaces_edit.md | 46 +++ ...oder_envs_ls.md => coder_workspaces_ls.md} | 12 +- ...rebuild.md => coder_workspaces_rebuild.md} | 12 +- docs/coder_workspaces_rm.md | 26 ++ docs/coder_workspaces_stop.md | 44 +++ ...ild.md => coder_workspaces_watch-build.md} | 10 +- internal/activity/pusher.go | 14 +- internal/cmd/ceapi.go | 70 ++-- internal/cmd/cmd.go | 5 +- internal/cmd/configssh.go | 68 ++-- internal/cmd/login.go | 22 +- internal/cmd/rebuild.go | 38 +- internal/cmd/resourcemanager.go | 158 ++++----- internal/cmd/resourcemanager_test.go | 28 +- internal/cmd/resourcemanager_test.golden | 24 +- internal/cmd/ssh.go | 28 +- internal/cmd/sync.go | 14 +- internal/cmd/tags.go | 2 +- internal/cmd/tokens.go | 2 +- internal/cmd/tunnel.go | 14 +- internal/cmd/urls.go | 72 ++-- internal/cmd/{envs.go => workspaces.go} | 331 +++++++++--------- .../cmd/{envs_test.go => workspaces_test.go} | 72 ++-- internal/coderutil/{env.go => workspace.go} | 68 ++-- internal/sync/singlefile.go | 6 +- internal/sync/sync.go | 14 +- pkg/clog/errgroup.go | 2 +- 57 files changed, 1003 insertions(+), 992 deletions(-) rename coder-sdk/{env.go => workspace.go} (52%) delete mode 100644 docs/coder_envs.md delete mode 100644 docs/coder_envs_create-from-config.md delete mode 100644 docs/coder_envs_create.md delete mode 100644 docs/coder_envs_edit.md delete mode 100644 docs/coder_envs_rm.md delete mode 100644 docs/coder_envs_stop.md create mode 100644 docs/coder_workspaces.md create mode 100644 docs/coder_workspaces_create-from-config.md create mode 100644 docs/coder_workspaces_create.md create mode 100644 docs/coder_workspaces_edit.md rename docs/{coder_envs_ls.md => coder_workspaces_ls.md} (51%) rename docs/{coder_envs_rebuild.md => coder_workspaces_rebuild.md} (59%) create mode 100644 docs/coder_workspaces_rm.md create mode 100644 docs/coder_workspaces_stop.md rename docs/{coder_envs_watch-build.md => coder_workspaces_watch-build.md} (53%) rename internal/cmd/{envs.go => workspaces.go} (60%) rename internal/cmd/{envs_test.go => workspaces_test.go} (61%) rename internal/coderutil/{env.go => workspace.go} (54%) diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index f92d733e..8654965a 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -53,15 +53,15 @@ func TestCoderCLI(t *testing.T) { headlessLogin(ctx, t, c) - c.Run(ctx, "coder envs").Assert(t, + c.Run(ctx, "coder workspaces").Assert(t, tcli.Success(), ) - c.Run(ctx, "coder envs ls").Assert(t, + c.Run(ctx, "coder workspaces ls").Assert(t, tcli.Success(), ) - c.Run(ctx, "coder envs ls -o json").Assert(t, + c.Run(ctx, "coder workspaces ls -o json").Assert(t, tcli.Success(), ) @@ -93,7 +93,7 @@ func TestCoderCLI(t *testing.T) { tcli.Success(), ) - c.Run(ctx, "coder envs ls").Assert(t, + c.Run(ctx, "coder workspaces ls").Assert(t, tcli.Error(), ) diff --git a/ci/integration/ssh_test.go b/ci/integration/ssh_test.go index 6a084a0d..5844ca93 100644 --- a/ci/integration/ssh_test.go +++ b/ci/integration/ssh_test.go @@ -13,17 +13,17 @@ func TestSSH(t *testing.T) { run(t, "ssh-coder-cli-tests", func(t *testing.T, ctx context.Context, c *tcli.ContainerRunner) { headlessLogin(ctx, t, c) - // TODO remove this once we can create an environment if there aren't any - var envs []coder.Environment - c.Run(ctx, "coder envs ls --output json").Assert(t, + // TODO remove this once we can create a workspace if there aren't any + var workspaces []coder.Workspace + c.Run(ctx, "coder workspaces ls --output json").Assert(t, tcli.Success(), - tcli.StdoutJSONUnmarshal(&envs), + tcli.StdoutJSONUnmarshal(&workspaces), ) assert := tcli.Success() - // if we don't have any environments, "coder config-ssh" will fail - if len(envs) == 0 { + // if we don't have any workspaces, "coder config-ssh" will fail + if len(workspaces) == 0 { assert = tcli.Error() } c.Run(ctx, "coder config-ssh").Assert(t, diff --git a/ci/integration/statictokens_test.go b/ci/integration/statictokens_test.go index d38dcd99..b1de474c 100644 --- a/ci/integration/statictokens_test.go +++ b/ci/integration/statictokens_test.go @@ -35,15 +35,15 @@ func TestStaticAuth(t *testing.T) { // make requests with token environment variable authentication cmd := exec.CommandContext(ctx, "sh", "-c", - fmt.Sprintf("export CODER_URL=%s && export CODER_TOKEN=$(cat) && coder envs ls", os.Getenv("CODER_URL")), + fmt.Sprintf("export CODER_URL=%s && export CODER_TOKEN=$(cat) && coder workspaces ls", os.Getenv("CODER_URL")), ) cmd.Stdin = strings.NewReader(string(result.Stdout)) c.RunCmd(cmd).Assert(t, tcli.Success(), ) - // should error when the environment variabels aren't set - c.Run(ctx, "coder envs ls").Assert(t, + // should error when the environment variables aren't set + c.Run(ctx, "coder workspaces ls").Assert(t, tcli.Error(), ) }) diff --git a/coder-sdk/activity.go b/coder-sdk/activity.go index c885f619..6d564efb 100644 --- a/coder-sdk/activity.go +++ b/coder-sdk/activity.go @@ -6,15 +6,15 @@ import ( ) type activityRequest struct { - Source string `json:"source"` - EnvironmentID string `json:"environment_id"` + Source string `json:"source"` + WorkspaceID string `json:"workspace_id"` } // PushActivity pushes CLI activity to Coder. -func (c *DefaultClient) PushActivity(ctx context.Context, source, envID string) error { +func (c *DefaultClient) PushActivity(ctx context.Context, source, workspaceID string) error { resp, err := c.request(ctx, http.MethodPost, "/api/private/metrics/usage/push", activityRequest{ - Source: source, - EnvironmentID: envID, + Source: source, + WorkspaceID: workspaceID, }) if err != nil { return err diff --git a/coder-sdk/activity_test.go b/coder-sdk/activity_test.go index 807b04f9..ff3083a3 100644 --- a/coder-sdk/activity_test.go +++ b/coder-sdk/activity_test.go @@ -17,14 +17,14 @@ func TestPushActivity(t *testing.T) { t.Parallel() const source = "test" - const envID = "602d377a-e6b8d763cae7561885c5f1b2" + const workspaceID = "602d377a-e6b8d763cae7561885c5f1b2" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PushActivity is a POST", http.MethodPost, r.Method) assert.Equal(t, "URL matches", "/api/private/metrics/usage/push", r.URL.Path) expected := map[string]interface{}{ - "source": source, - "environment_id": envID, + "source": source, + "workspace_id": workspaceID, } var request map[string]interface{} err := json.NewDecoder(r.Body).Decode(&request) @@ -46,6 +46,6 @@ func TestPushActivity(t *testing.T) { }) assert.Success(t, "failed to create coder.Client", err) - err = client.PushActivity(context.Background(), source, envID) + err = client.PushActivity(context.Background(), source, workspaceID) assert.Success(t, "expected successful response from PushActivity", err) } diff --git a/coder-sdk/devurl.go b/coder-sdk/devurl.go index 27a18366..af6cf64f 100644 --- a/coder-sdk/devurl.go +++ b/coder-sdk/devurl.go @@ -17,38 +17,38 @@ type DevURL struct { } type delDevURLRequest struct { - EnvID string `json:"environment_id"` - DevURLID string `json:"url_id"` + WorkspaceID string `json:"workspace_id"` + DevURLID string `json:"url_id"` } // DeleteDevURL deletes the specified devurl. -func (c *DefaultClient) DeleteDevURL(ctx context.Context, envID, urlID string) error { - reqURL := fmt.Sprintf("/api/v0/environments/%s/devurls/%s", envID, urlID) +func (c *DefaultClient) DeleteDevURL(ctx context.Context, workspaceID, urlID string) error { + reqURL := fmt.Sprintf("/api/v0/workspaces/%s/devurls/%s", workspaceID, urlID) return c.requestBody(ctx, http.MethodDelete, reqURL, delDevURLRequest{ - EnvID: envID, - DevURLID: urlID, + WorkspaceID: workspaceID, + DevURLID: urlID, }, nil) } // CreateDevURLReq defines the request parameters for creating a new DevURL. type CreateDevURLReq struct { - EnvID string `json:"environment_id"` - Port int `json:"port"` - Access string `json:"access"` - Name string `json:"name"` - Scheme string `json:"scheme"` + WorkspaceID string `json:"workspace_id"` + Port int `json:"port"` + Access string `json:"access"` + Name string `json:"name"` + Scheme string `json:"scheme"` } // CreateDevURL inserts a new dev URL for the authenticated user. -func (c *DefaultClient) CreateDevURL(ctx context.Context, envID string, req CreateDevURLReq) error { - return c.requestBody(ctx, http.MethodPost, "/api/v0/environments/"+envID+"/devurls", req, nil) +func (c *DefaultClient) CreateDevURL(ctx context.Context, workspaceID string, req CreateDevURLReq) error { + return c.requestBody(ctx, http.MethodPost, "/api/v0/workspaces/"+workspaceID+"/devurls", req, nil) } -// DevURLs fetches the Dev URLs for a given environment. -func (c *DefaultClient) DevURLs(ctx context.Context, envID string) ([]DevURL, error) { +// DevURLs fetches the Dev URLs for a given workspace. +func (c *DefaultClient) DevURLs(ctx context.Context, workspaceID string) ([]DevURL, error) { var devurls []DevURL - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/environments/"+envID+"/devurls", nil, &devurls); err != nil { + if err := c.requestBody(ctx, http.MethodGet, "/api/v0/workspaces/"+workspaceID+"/devurls", nil, &devurls); err != nil { return nil, err } return devurls, nil @@ -58,6 +58,6 @@ func (c *DefaultClient) DevURLs(ctx context.Context, envID string) ([]DevURL, er type PutDevURLReq CreateDevURLReq // PutDevURL updates an existing devurl for the authenticated user. -func (c *DefaultClient) PutDevURL(ctx context.Context, envID, urlID string, req PutDevURLReq) error { - return c.requestBody(ctx, http.MethodPut, "/api/v0/environments/"+envID+"/devurls/"+urlID, req, nil) +func (c *DefaultClient) PutDevURL(ctx context.Context, workspaceID, urlID string, req PutDevURLReq) error { + return c.requestBody(ctx, http.MethodPut, "/api/v0/workspaces/"+workspaceID+"/devurls/"+urlID, req, nil) } diff --git a/coder-sdk/interface.go b/coder-sdk/interface.go index 42c34d2c..47ed28c3 100644 --- a/coder-sdk/interface.go +++ b/coder-sdk/interface.go @@ -12,7 +12,7 @@ import ( // This is an interface to allow for mocking of coder-sdk client usage. type Client interface { // PushActivity pushes CLI activity to Coder. - PushActivity(ctx context.Context, source, envID string) error + PushActivity(ctx context.Context, source, workspaceID string) error // Me gets the details of the authenticated user. Me(ctx context.Context) (*User, error) @@ -66,75 +66,75 @@ type Client interface { SiteConfigWorkspaces(ctx context.Context) (*ConfigWorkspaces, error) // DeleteDevURL deletes the specified devurl. - DeleteDevURL(ctx context.Context, envID, urlID string) error + DeleteDevURL(ctx context.Context, workspaceID, urlID string) error // CreateDevURL inserts a new devurl for the authenticated user. - CreateDevURL(ctx context.Context, envID string, req CreateDevURLReq) error + CreateDevURL(ctx context.Context, workspaceID string, req CreateDevURLReq) error - // DevURLs fetches the Dev URLs for a given environment. - DevURLs(ctx context.Context, envID string) ([]DevURL, error) + // DevURLs fetches the Dev URLs for a given workspace. + DevURLs(ctx context.Context, workspaceID string) ([]DevURL, error) // PutDevURL updates an existing devurl for the authenticated user. - PutDevURL(ctx context.Context, envID, urlID string, req PutDevURLReq) error + PutDevURL(ctx context.Context, workspaceID, urlID string, req PutDevURLReq) error - // CreateEnvironment sends a request to create an environment. - CreateEnvironment(ctx context.Context, req CreateEnvironmentRequest) (*Environment, error) + // CreateWorkspace sends a request to create a workspace. + CreateWorkspace(ctx context.Context, req CreateWorkspaceRequest) (*Workspace, error) // ParseTemplate parses a template config. It support both remote repositories and local files. // If a local file is specified then all other values in the request are ignored. ParseTemplate(ctx context.Context, req ParseTemplateRequest) (*TemplateVersion, error) - // CreateEnvironmentFromRepo sends a request to create an environment from a repository. - CreateEnvironmentFromRepo(ctx context.Context, orgID string, req TemplateVersion) (*Environment, error) + // CreateWorkspaceFromRepo sends a request to create a workspace from a repository. + CreateWorkspaceFromRepo(ctx context.Context, orgID string, req TemplateVersion) (*Workspace, error) - // Environments lists environments returned by the given filter. - Environments(ctx context.Context) ([]Environment, error) + // Workspaces lists workspaces returned by the given filter. + Workspaces(ctx context.Context) ([]Workspace, error) - // UserEnvironmentsByOrganization gets the list of environments owned by the given user. - UserEnvironmentsByOrganization(ctx context.Context, userID, orgID string) ([]Environment, error) + // UserWorkspacesByOrganization gets the list of workspaces owned by the given user. + UserWorkspacesByOrganization(ctx context.Context, userID, orgID string) ([]Workspace, error) - // DeleteEnvironment deletes the environment. - DeleteEnvironment(ctx context.Context, envID string) error + // DeleteWorkspace deletes the workspace. + DeleteWorkspace(ctx context.Context, workspaceID string) error - // StopEnvironment stops the environment. - StopEnvironment(ctx context.Context, envID string) error + // StopWorkspace stops the workspace. + StopWorkspace(ctx context.Context, workspaceID string) error - // RebuildEnvironment requests that the given envID is rebuilt with no changes to its specification. - RebuildEnvironment(ctx context.Context, envID string) error + // RebuildWorkspace requests that the given workspaceID is rebuilt with no changes to its specification. + RebuildWorkspace(ctx context.Context, workspaceID string) error - // EditEnvironment modifies the environment specification and initiates a rebuild. - EditEnvironment(ctx context.Context, envID string, req UpdateEnvironmentReq) error + // EditWorkspace modifies the workspace specification and initiates a rebuild. + EditWorkspace(ctx context.Context, workspaceID string, req UpdateWorkspaceReq) error - // DialWsep dials an environments command execution interface + // DialWsep dials a workspace's command execution interface // See https://github.com/cdr/wsep for details. - DialWsep(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) + DialWsep(ctx context.Context, baseURL *url.URL, workspaceID string) (*websocket.Conn, error) - // DialExecutor gives a remote execution interface for performing commands inside an environment. - DialExecutor(ctx context.Context, baseURL *url.URL, envID string) (wsep.Execer, error) + // DialExecutor gives a remote execution interface for performing commands inside a workspace. + DialExecutor(ctx context.Context, baseURL *url.URL, workspaceID string) (wsep.Execer, error) - // DialIDEStatus opens a websocket connection for cpu load metrics on the environment. - DialIDEStatus(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) + // DialIDEStatus opens a websocket connection for cpu load metrics on the workspace. + DialIDEStatus(ctx context.Context, baseURL *url.URL, workspaceID string) (*websocket.Conn, error) - // DialEnvironmentBuildLog opens a websocket connection for the environment build log messages. - DialEnvironmentBuildLog(ctx context.Context, envID string) (*websocket.Conn, error) + // DialWorkspaceBuildLog opens a websocket connection for the workspace build log messages. + DialWorkspaceBuildLog(ctx context.Context, workspaceID string) (*websocket.Conn, error) - // FollowEnvironmentBuildLog trails the build log of a Coder environment. - FollowEnvironmentBuildLog(ctx context.Context, envID string) (<-chan BuildLogFollowMsg, error) + // FollowWorkspaceBuildLog trails the build log of a Coder workspace. + FollowWorkspaceBuildLog(ctx context.Context, workspaceID string) (<-chan BuildLogFollowMsg, error) - // DialEnvironmentStats opens a websocket connection for environment stats. - DialEnvironmentStats(ctx context.Context, envID string) (*websocket.Conn, error) + // DialWorkspaceStats opens a websocket connection for workspace stats. + DialWorkspaceStats(ctx context.Context, workspaceID string) (*websocket.Conn, error) - // DialResourceLoad opens a websocket connection for cpu load metrics on the environment. - DialResourceLoad(ctx context.Context, envID string) (*websocket.Conn, error) + // DialResourceLoad opens a websocket connection for cpu load metrics on the workspace. + DialResourceLoad(ctx context.Context, workspaceID string) (*websocket.Conn, error) - // WaitForEnvironmentReady will watch the build log and return when done. - WaitForEnvironmentReady(ctx context.Context, envID string) error + // WaitForWorkspaceReady will watch the build log and return when done. + WaitForWorkspaceReady(ctx context.Context, workspaceID string) error - // EnvironmentByID get the details of an environment by its id. - EnvironmentByID(ctx context.Context, id string) (*Environment, error) + // WorkspaceByID get the details of a workspace by its id. + WorkspaceByID(ctx context.Context, id string) (*Workspace, error) - // EnvironmentsByWorkspaceProvider returns environments that belong to a particular workspace provider. - EnvironmentsByWorkspaceProvider(ctx context.Context, wpID string) ([]Environment, error) + // WorkspacesByWorkspaceProvider returns workspaces that belong to a particular workspace provider. + WorkspacesByWorkspaceProvider(ctx context.Context, wpID string) ([]Workspace, error) // ImportImage creates a new image and optionally a new registry. ImportImage(ctx context.Context, req ImportImageReq) (*Image, error) diff --git a/coder-sdk/org.go b/coder-sdk/org.go index 92718dcd..0922d229 100644 --- a/coder-sdk/org.go +++ b/coder-sdk/org.go @@ -13,7 +13,7 @@ type Organization struct { Description string `json:"description"` Default bool `json:"default"` Members []OrganizationUser `json:"members"` - EnvironmentCount int `json:"environment_count"` + WorkspaceCount int `json:"workspace_count"` ResourceNamespace string `json:"resource_namespace"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` diff --git a/coder-sdk/tags.go b/coder-sdk/tags.go index 7e4563a9..9a3c941f 100644 --- a/coder-sdk/tags.go +++ b/coder-sdk/tags.go @@ -8,14 +8,14 @@ import ( // ImageTag is a Docker image tag. type ImageTag struct { - ImageID string `json:"image_id" table:"-"` - Tag string `json:"tag" table:"Tag"` - LatestHash string `json:"latest_hash" table:"-"` - HashLastUpdatedAt time.Time `json:"hash_last_updated_at" table:"-"` - OSRelease *OSRelease `json:"os_release" table:"OS"` - Environments []*Environment `json:"environments" table:"-"` - UpdatedAt time.Time `json:"updated_at" table:"UpdatedAt"` - CreatedAt time.Time `json:"created_at" table:"-"` + ImageID string `json:"image_id" table:"-"` + Tag string `json:"tag" table:"Tag"` + LatestHash string `json:"latest_hash" table:"-"` + HashLastUpdatedAt time.Time `json:"hash_last_updated_at" table:"-"` + OSRelease *OSRelease `json:"os_release" table:"OS"` + Workspaces []*Workspace `json:"workspaces" table:"-"` + UpdatedAt time.Time `json:"updated_at" table:"UpdatedAt"` + CreatedAt time.Time `json:"created_at" table:"-"` } func (i ImageTag) String() string { diff --git a/coder-sdk/env.go b/coder-sdk/workspace.go similarity index 52% rename from coder-sdk/env.go rename to coder-sdk/workspace.go index e807b219..fca61dcd 100644 --- a/coder-sdk/env.go +++ b/coder-sdk/workspace.go @@ -13,8 +13,8 @@ import ( "nhooyr.io/websocket/wsjson" ) -// Environment describes a Coder environment. -type Environment struct { +// Workspace describes a Coder workspace. +type Workspace struct { ID string `json:"id" table:"-"` Name string `json:"name" table:"Name"` ImageID string `json:"image_id" table:"-"` @@ -27,7 +27,7 @@ type Environment struct { DiskGB int `json:"disk_gb" table:"DiskGB"` GPUs int `json:"gpus" table:"-"` Updating bool `json:"updating" table:"-"` - LatestStat EnvironmentStat `json:"latest_stat" table:"Status"` + LatestStat WorkspaceStat `json:"latest_stat" table:"Status"` RebuildMessages []RebuildMessage `json:"rebuild_messages" table:"-"` CreatedAt time.Time `json:"created_at" table:"-"` UpdatedAt time.Time `json:"updated_at" table:"-"` @@ -38,42 +38,42 @@ type Environment struct { ResourcePoolID string `json:"resource_pool_id" table:"-"` } -// RebuildMessage defines the message shown when an Environment requires a rebuild for it can be accessed. +// RebuildMessage defines the message shown when a Workspace requires a rebuild for it can be accessed. type RebuildMessage struct { Text string `json:"text"` Required bool `json:"required"` AutoOffThreshold Duration `json:"auto_off_threshold"` } -// EnvironmentStat represents the state of an environment. -type EnvironmentStat struct { - Time time.Time `json:"time"` - LastOnline time.Time `json:"last_online"` - ContainerStatus EnvironmentStatus `json:"container_status"` - StatError string `json:"stat_error"` - CPUUsage float32 `json:"cpu_usage"` - MemoryTotal int64 `json:"memory_total"` - MemoryUsage float32 `json:"memory_usage"` - DiskTotal int64 `json:"disk_total"` - DiskUsed int64 `json:"disk_used"` +// WorkspaceStat represents the state of a workspace. +type WorkspaceStat struct { + Time time.Time `json:"time"` + LastOnline time.Time `json:"last_online"` + ContainerStatus WorkspaceStatus `json:"container_status"` + StatError string `json:"stat_error"` + CPUUsage float32 `json:"cpu_usage"` + MemoryTotal int64 `json:"memory_total"` + MemoryUsage float32 `json:"memory_usage"` + DiskTotal int64 `json:"disk_total"` + DiskUsed int64 `json:"disk_used"` } -func (e EnvironmentStat) String() string { return string(e.ContainerStatus) } +func (e WorkspaceStat) String() string { return string(e.ContainerStatus) } -// EnvironmentStatus refers to the states of an environment. -type EnvironmentStatus string +// WorkspaceStatus refers to the states of a workspace. +type WorkspaceStatus string -// The following represent the possible environment container states. +// The following represent the possible workspace container states. const ( - EnvironmentCreating EnvironmentStatus = "CREATING" - EnvironmentOff EnvironmentStatus = "OFF" - EnvironmentOn EnvironmentStatus = "ON" - EnvironmentFailed EnvironmentStatus = "FAILED" - EnvironmentUnknown EnvironmentStatus = "UNKNOWN" + WorkspaceCreating WorkspaceStatus = "CREATING" + WorkspaceOff WorkspaceStatus = "OFF" + WorkspaceOn WorkspaceStatus = "ON" + WorkspaceFailed WorkspaceStatus = "FAILED" + WorkspaceUnknown WorkspaceStatus = "UNKNOWN" ) -// CreateEnvironmentRequest is used to configure a new environment. -type CreateEnvironmentRequest struct { +// CreateWorkspaceRequest is used to configure a new workspace. +type CreateWorkspaceRequest struct { Name string `json:"name"` ImageID string `json:"image_id"` OrgID string `json:"org_id"` @@ -91,13 +91,13 @@ type CreateEnvironmentRequest struct { TemplateID string `json:"template_id,omitempty"` } -// CreateEnvironment sends a request to create an environment. -func (c *DefaultClient) CreateEnvironment(ctx context.Context, req CreateEnvironmentRequest) (*Environment, error) { - var env Environment - if err := c.requestBody(ctx, http.MethodPost, "/api/v0/environments", req, &env); err != nil { +// CreateWorkspace sends a request to create a workspace. +func (c *DefaultClient) CreateWorkspace(ctx context.Context, req CreateWorkspaceRequest) (*Workspace, error) { + var workspace Workspace + if err := c.requestBody(ctx, http.MethodPost, "/api/v0/workspaces", req, &workspace); err != nil { return nil, err } - return &env, nil + return &workspace, nil } // ParseTemplateRequest parses a template. If Local is a non-nil reader @@ -112,7 +112,7 @@ type ParseTemplateRequest struct { // TemplateVersion is a Workspaces As Code (WAC) template. // For now, let's not interpret it on the CLI level. We just need -// to forward this as part of the create env request. +// to forward this as part of the create workspace request. type TemplateVersion struct { ID string `json:"id"` TemplateID string `json:"template_id"` @@ -127,7 +127,7 @@ type TemplateVersion struct { // ParseTemplate parses a template config. It support both remote repositories and local files. // If a local file is specified then all other values in the request are ignored. func (c *DefaultClient) ParseTemplate(ctx context.Context, req ParseTemplateRequest) (*TemplateVersion, error) { - const path = "/api/private/environments/template/parse" + const path = "/api/private/workspaces/template/parse" var ( tpl TemplateVersion opts []requestOption @@ -157,54 +157,54 @@ func (c *DefaultClient) ParseTemplate(ctx context.Context, req ParseTemplateRequ return &tpl, nil } -// CreateEnvironmentFromRepo sends a request to create an environment from a repository. -func (c *DefaultClient) CreateEnvironmentFromRepo(ctx context.Context, orgID string, req TemplateVersion) (*Environment, error) { - var env Environment - if err := c.requestBody(ctx, http.MethodPost, "/api/private/orgs/"+orgID+"/environments/from-repo", req, &env); err != nil { +// CreateWorkspaceFromRepo sends a request to create a workspace from a repository. +func (c *DefaultClient) CreateWorkspaceFromRepo(ctx context.Context, orgID string, req TemplateVersion) (*Workspace, error) { + var workspace Workspace + if err := c.requestBody(ctx, http.MethodPost, "/api/private/orgs/"+orgID+"/workspaces/from-repo", req, &workspace); err != nil { return nil, err } - return &env, nil + return &workspace, nil } -// Environments lists environments returned by the given filter. +// Workspaces lists workspaces returned by the given filter. // TODO: add the filter options, explore performance issue. -func (c *DefaultClient) Environments(ctx context.Context) ([]Environment, error) { - var envs []Environment - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/environments", nil, &envs); err != nil { +func (c *DefaultClient) Workspaces(ctx context.Context) ([]Workspace, error) { + var workspaces []Workspace + if err := c.requestBody(ctx, http.MethodGet, "/api/v0/workspaces", nil, &workspaces); err != nil { return nil, err } - return envs, nil + return workspaces, nil } -// UserEnvironmentsByOrganization gets the list of environments owned by the given user. -func (c *DefaultClient) UserEnvironmentsByOrganization(ctx context.Context, userID, orgID string) ([]Environment, error) { +// UserWorkspacesByOrganization gets the list of workspaces owned by the given user. +func (c *DefaultClient) UserWorkspacesByOrganization(ctx context.Context, userID, orgID string) ([]Workspace, error) { var ( - envs []Environment - query = url.Values{} + workspaces []Workspace + query = url.Values{} ) query.Add("orgs", orgID) query.Add("users", userID) - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/environments", nil, &envs, withQueryParams(query)); err != nil { + if err := c.requestBody(ctx, http.MethodGet, "/api/v0/workspaces", nil, &workspaces, withQueryParams(query)); err != nil { return nil, err } - return envs, nil + return workspaces, nil } -// DeleteEnvironment deletes the environment. -func (c *DefaultClient) DeleteEnvironment(ctx context.Context, envID string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/v0/environments/"+envID, nil, nil) +// DeleteWorkspace deletes the workspace. +func (c *DefaultClient) DeleteWorkspace(ctx context.Context, workspaceID string) error { + return c.requestBody(ctx, http.MethodDelete, "/api/v0/workspaces/"+workspaceID, nil, nil) } -// StopEnvironment stops the environment. -func (c *DefaultClient) StopEnvironment(ctx context.Context, envID string) error { - return c.requestBody(ctx, http.MethodPut, "/api/v0/environments/"+envID+"/stop", nil, nil) +// StopWorkspace stops the workspace. +func (c *DefaultClient) StopWorkspace(ctx context.Context, workspaceID string) error { + return c.requestBody(ctx, http.MethodPut, "/api/v0/workspaces/"+workspaceID+"/stop", nil, nil) } -// UpdateEnvironmentReq defines the update operation, only setting +// UpdateWorkspaceReq defines the update operation, only setting // nil-fields. -type UpdateEnvironmentReq struct { +type UpdateWorkspaceReq struct { ImageID *string `json:"image_id"` ImageTag *string `json:"image_tag"` CPUCores *float32 `json:"cpu_cores"` @@ -213,45 +213,46 @@ type UpdateEnvironmentReq struct { GPUs *int `json:"gpus"` } -// RebuildEnvironment requests that the given envID is rebuilt with no changes to its specification. -func (c *DefaultClient) RebuildEnvironment(ctx context.Context, envID string) error { - return c.requestBody(ctx, http.MethodPatch, "/api/v0/environments/"+envID, UpdateEnvironmentReq{}, nil) +// RebuildWorkspace requests that the given workspaceID is rebuilt with no changes to its specification. +func (c *DefaultClient) RebuildWorkspace(ctx context.Context, workspaceID string) error { + return c.requestBody(ctx, http.MethodPatch, "/api/v0/workspaces/"+workspaceID, UpdateWorkspaceReq{}, nil) } -// EditEnvironment modifies the environment specification and initiates a rebuild. -func (c *DefaultClient) EditEnvironment(ctx context.Context, envID string, req UpdateEnvironmentReq) error { - return c.requestBody(ctx, http.MethodPatch, "/api/v0/environments/"+envID, req, nil) +// EditWorkspace modifies the workspace specification and initiates a rebuild. +func (c *DefaultClient) EditWorkspace(ctx context.Context, workspaceID string, req UpdateWorkspaceReq) error { + return c.requestBody(ctx, http.MethodPatch, "/api/v0/workspaces/"+workspaceID, req, nil) } -// DialWsep dials an environments command execution interface +// DialWsep dials a workspace's command execution interface // See https://github.com/cdr/wsep for details. -func (c *DefaultClient) DialWsep(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/proxy/environments/"+envID+"/wsep", withBaseURL(baseURL)) +func (c *DefaultClient) DialWsep(ctx context.Context, baseURL *url.URL, workspaceID string) (*websocket.Conn, error) { + return c.dialWebsocket(ctx, "/proxy/workspaces/"+workspaceID+"/wsep", withBaseURL(baseURL)) } -// DialExecutor gives a remote execution interface for performing commands inside an environment. -func (c *DefaultClient) DialExecutor(ctx context.Context, baseURL *url.URL, envID string) (wsep.Execer, error) { - ws, err := c.DialWsep(ctx, baseURL, envID) +// DialExecutor gives a remote execution interface for performing commands +// inside a workspace. +func (c *DefaultClient) DialExecutor(ctx context.Context, baseURL *url.URL, workspaceID string) (wsep.Execer, error) { + ws, err := c.DialWsep(ctx, baseURL, workspaceID) if err != nil { return nil, err } return wsep.RemoteExecer(ws), nil } -// DialIDEStatus opens a websocket connection for cpu load metrics on the environment. -func (c *DefaultClient) DialIDEStatus(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/proxy/environments/"+envID+"/ide/api/status", withBaseURL(baseURL)) +// DialIDEStatus opens a websocket connection for cpu load metrics on the workspace. +func (c *DefaultClient) DialIDEStatus(ctx context.Context, baseURL *url.URL, workspaceID string) (*websocket.Conn, error) { + return c.dialWebsocket(ctx, "/proxy/workspaces/"+workspaceID+"/ide/api/status", withBaseURL(baseURL)) } -// DialEnvironmentBuildLog opens a websocket connection for the environment build log messages. -func (c *DefaultClient) DialEnvironmentBuildLog(ctx context.Context, envID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/api/private/environments/"+envID+"/watch-update") +// DialWorkspaceBuildLog opens a websocket connection for the workspace build log messages. +func (c *DefaultClient) DialWorkspaceBuildLog(ctx context.Context, workspaceID string) (*websocket.Conn, error) { + return c.dialWebsocket(ctx, "/api/private/workspaces/"+workspaceID+"/watch-update") } -// BuildLog defines a build log record for a Coder environment. +// BuildLog defines a build log record for a Coder workspace. type BuildLog struct { - ID string `db:"id" json:"id"` - EnvironmentID string `db:"environment_id" json:"environment_id"` + ID string `db:"id" json:"id"` + WorkspaceID string `db:"workspace_id" json:"workspace_id"` // BuildID allows the frontend to separate the logs from the old build with the logs from the new. BuildID string `db:"build_id" json:"build_id"` Time time.Time `db:"time" json:"time"` @@ -266,10 +267,10 @@ type BuildLogFollowMsg struct { Err error } -// FollowEnvironmentBuildLog trails the build log of a Coder environment. -func (c *DefaultClient) FollowEnvironmentBuildLog(ctx context.Context, envID string) (<-chan BuildLogFollowMsg, error) { +// FollowWorkspaceBuildLog trails the build log of a Coder workspace. +func (c *DefaultClient) FollowWorkspaceBuildLog(ctx context.Context, workspaceID string) (<-chan BuildLogFollowMsg, error) { ch := make(chan BuildLogFollowMsg) - ws, err := c.DialEnvironmentBuildLog(ctx, envID) + ws, err := c.DialWorkspaceBuildLog(ctx, workspaceID) if err != nil { return nil, err } @@ -291,14 +292,14 @@ func (c *DefaultClient) FollowEnvironmentBuildLog(ctx context.Context, envID str return ch, nil } -// DialEnvironmentStats opens a websocket connection for environment stats. -func (c *DefaultClient) DialEnvironmentStats(ctx context.Context, envID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/api/private/environments/"+envID+"/watch-stats") +// DialWorkspaceStats opens a websocket connection for workspace stats. +func (c *DefaultClient) DialWorkspaceStats(ctx context.Context, workspaceID string) (*websocket.Conn, error) { + return c.dialWebsocket(ctx, "/api/private/workspaces/"+workspaceID+"/watch-stats") } -// DialResourceLoad opens a websocket connection for cpu load metrics on the environment. -func (c *DefaultClient) DialResourceLoad(ctx context.Context, envID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/api/private/environments/"+envID+"/watch-resource-load") +// DialResourceLoad opens a websocket connection for cpu load metrics on the workspace. +func (c *DefaultClient) DialResourceLoad(ctx context.Context, workspaceID string) (*websocket.Conn, error) { + return c.dialWebsocket(ctx, "/api/private/workspaces/"+workspaceID+"/watch-resource-load") } // BuildLogType describes the type of an event. @@ -307,8 +308,8 @@ type BuildLogType string const ( // BuildLogTypeStart signals that a new build log has begun. BuildLogTypeStart BuildLogType = "start" - // BuildLogTypeStage is a stage-level event for an environment. - // It can be thought of as a major step in the environment's + // BuildLogTypeStage is a stage-level event for a workspace. + // It can be thought of as a major step in the workspace's // lifecycle. BuildLogTypeStage BuildLogType = "stage" // BuildLogTypeError describes an error that has occurred. @@ -325,18 +326,18 @@ type buildLogMsg struct { Type BuildLogType `json:"type"` } -// WaitForEnvironmentReady will watch the build log and return when done. -func (c *DefaultClient) WaitForEnvironmentReady(ctx context.Context, envID string) error { - conn, err := c.DialEnvironmentBuildLog(ctx, envID) +// WaitForWorkspaceReady will watch the build log and return when done. +func (c *DefaultClient) WaitForWorkspaceReady(ctx context.Context, workspaceID string) error { + conn, err := c.DialWorkspaceBuildLog(ctx, workspaceID) if err != nil { - return xerrors.Errorf("%s: dial build log: %w", envID, err) + return xerrors.Errorf("%s: dial build log: %w", workspaceID, err) } for { msg := buildLogMsg{} err := wsjson.Read(ctx, conn, &msg) if err != nil { - return xerrors.Errorf("%s: reading build log msg: %w", envID, err) + return xerrors.Errorf("%s: reading build log msg: %w", workspaceID, err) } if msg.Type == BuildLogTypeDone { @@ -345,20 +346,20 @@ func (c *DefaultClient) WaitForEnvironmentReady(ctx context.Context, envID strin } } -// EnvironmentByID get the details of an environment by its id. -func (c *DefaultClient) EnvironmentByID(ctx context.Context, id string) (*Environment, error) { - var env Environment - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/environments/"+id, nil, &env); err != nil { +// WorkspaceByID get the details of a workspace by its id. +func (c *DefaultClient) WorkspaceByID(ctx context.Context, id string) (*Workspace, error) { + var workspace Workspace + if err := c.requestBody(ctx, http.MethodGet, "/api/v0/workspaces/"+id, nil, &workspace); err != nil { return nil, err } - return &env, nil + return &workspace, nil } -// EnvironmentsByWorkspaceProvider returns all environments that belong to a particular workspace provider. -func (c *DefaultClient) EnvironmentsByWorkspaceProvider(ctx context.Context, wpID string) ([]Environment, error) { - var envs []Environment - if err := c.requestBody(ctx, http.MethodGet, "/api/private/resource-pools/"+wpID+"/environments", nil, &envs); err != nil { +// WorkspacesByWorkspaceProvider returns all workspaces that belong to a particular workspace provider. +func (c *DefaultClient) WorkspacesByWorkspaceProvider(ctx context.Context, wpID string) ([]Workspace, error) { + var workspaces []Workspace + if err := c.requestBody(ctx, http.MethodGet, "/api/private/resource-pools/"+wpID+"/workspaces", nil, &workspaces); err != nil { return nil, err } - return envs, nil + return workspaces, nil } diff --git a/coder-sdk/workspace_providers.go b/coder-sdk/workspace_providers.go index 28710867..1ed0589c 100644 --- a/coder-sdk/workspace_providers.go +++ b/coder-sdk/workspace_providers.go @@ -10,7 +10,7 @@ type WorkspaceProviders struct { Kubernetes []KubernetesProvider `json:"kubernetes"` } -// KubernetesProvider defines an entity capable of deploying and acting as an ingress for Coder environments. +// KubernetesProvider defines an entity capable of deploying and acting as an ingress for Coder workspaces. type KubernetesProvider struct { ID string `json:"id" table:"-"` Name string `json:"name" table:"Name"` diff --git a/docs/coder.md b/docs/coder.md index bd37b0bd..ab6254e8 100644 --- a/docs/coder.md +++ b/docs/coder.md @@ -12,14 +12,14 @@ coder provides a CLI for working with an existing Coder installation ### SEE ALSO * [coder completion](coder_completion.md) - Generate completion script -* [coder config-ssh](coder_config-ssh.md) - Configure SSH to access Coder environments -* [coder envs](coder_envs.md) - Interact with Coder environments +* [coder config-ssh](coder_config-ssh.md) - Configure SSH to access Coder workspaces * [coder images](coder_images.md) - Manage Coder images * [coder login](coder_login.md) - Authenticate this client for future operations * [coder logout](coder_logout.md) - Remove local authentication credentials if any exist -* [coder ssh](coder_ssh.md) - Enter a shell of execute a command over SSH into a Coder environment -* [coder sync](coder_sync.md) - Establish a one way directory sync to a Coder environment +* [coder ssh](coder_ssh.md) - Enter a shell of execute a command over SSH into a Coder workspace +* [coder sync](coder_sync.md) - Establish a one way directory sync to a Coder workspace * [coder tokens](coder_tokens.md) - manage Coder API tokens for the active user -* [coder urls](coder_urls.md) - Interact with environment DevURLs +* [coder urls](coder_urls.md) - Interact with workspace DevURLs * [coder users](coder_users.md) - Interact with Coder user accounts +* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces diff --git a/docs/coder_completion.md b/docs/coder_completion.md index 7478c155..143dcac7 100644 --- a/docs/coder_completion.md +++ b/docs/coder_completion.md @@ -18,7 +18,7 @@ MacOS: Zsh: -If shell completion is not already enabled in your environment you will need +If shell completion is not already enabled in your workspace you will need to enable it. You can execute the following once: $ echo "autoload -U compinit; compinit" >> ~/.zshrc diff --git a/docs/coder_config-ssh.md b/docs/coder_config-ssh.md index 7aba039f..c16d087b 100644 --- a/docs/coder_config-ssh.md +++ b/docs/coder_config-ssh.md @@ -1,6 +1,6 @@ ## coder config-ssh -Configure SSH to access Coder environments +Configure SSH to access Coder workspaces ### Synopsis diff --git a/docs/coder_envs.md b/docs/coder_envs.md deleted file mode 100644 index 623c5e83..00000000 --- a/docs/coder_envs.md +++ /dev/null @@ -1,32 +0,0 @@ -## coder envs - -Interact with Coder environments - -### Synopsis - -Perform operations on the Coder environments owned by the active user. - -### Options - -``` - -h, --help help for envs -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation -* [coder envs create](coder_envs_create.md) - create a new environment. -* [coder envs create-from-config](coder_envs_create-from-config.md) - create a new environment from a template -* [coder envs edit](coder_envs_edit.md) - edit an existing environment and initiate a rebuild. -* [coder envs ls](coder_envs_ls.md) - list all environments owned by the active user -* [coder envs rebuild](coder_envs_rebuild.md) - rebuild a Coder environment -* [coder envs rm](coder_envs_rm.md) - remove Coder environments by name -* [coder envs stop](coder_envs_stop.md) - stop Coder environments by name -* [coder envs watch-build](coder_envs_watch-build.md) - trail the build log of a Coder environment - diff --git a/docs/coder_envs_create-from-config.md b/docs/coder_envs_create-from-config.md deleted file mode 100644 index e90e7bfe..00000000 --- a/docs/coder_envs_create-from-config.md +++ /dev/null @@ -1,43 +0,0 @@ -## coder envs create-from-config - -create a new environment from a template - -### Synopsis - -Create a new Coder environment using a Workspaces As Code template. - -``` -coder envs create-from-config [flags] -``` - -### Examples - -``` -# create a new environment from git repository -coder envs create-from-config --name="dev-env" --repo-url https://github.com/cdr/m --ref my-branch -coder envs create-from-config --name="dev-env" -f coder.yaml -``` - -### Options - -``` - -f, --filepath string path to local template file. - --follow follow buildlog after initiating rebuild - -h, --help help for create-from-config - --name string name of the environment to be created - -o, --org string name of the organization the environment should be created under. - --provider string name of Workspace Provider with which to create the environment - --ref string git reference to pull template from. May be a branch, tag, or commit hash. (default "master") - -r, --repo-url string URL of the git repository to pull the config from. Config file must live in '.coder/coder.yaml'. -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder envs](coder_envs.md) - Interact with Coder environments - diff --git a/docs/coder_envs_create.md b/docs/coder_envs_create.md deleted file mode 100644 index bc1f6517..00000000 --- a/docs/coder_envs_create.md +++ /dev/null @@ -1,47 +0,0 @@ -## coder envs create - -create a new environment. - -### Synopsis - -Create a new Coder environment. - -``` -coder envs create [environment_name] [flags] -``` - -### Examples - -``` -# create a new environment using default resource amounts -coder envs create my-new-env --image ubuntu -coder envs create my-new-powerful-env --cpu 12 --disk 100 --memory 16 --image ubuntu -``` - -### Options - -``` - --container-based-vm deploy the environment as a Container-based VM - -c, --cpu float32 number of cpu cores the environment should be provisioned with. - -d, --disk int GB of disk storage an environment should be provisioned with. - --enable-autostart automatically start this environment at your preferred time. - --follow follow buildlog after initiating rebuild - -g, --gpus int number GPUs an environment should be provisioned with. - -h, --help help for create - -i, --image string name of the image to base the environment off of. - -m, --memory float32 GB of RAM an environment should be provisioned with. - -o, --org string name of the organization the environment should be created under. - --provider string name of Workspace Provider with which to create the environment - -t, --tag string tag of the image the environment will be based off of. (default "latest") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder envs](coder_envs.md) - Interact with Coder environments - diff --git a/docs/coder_envs_edit.md b/docs/coder_envs_edit.md deleted file mode 100644 index a8cc2551..00000000 --- a/docs/coder_envs_edit.md +++ /dev/null @@ -1,46 +0,0 @@ -## coder envs edit - -edit an existing environment and initiate a rebuild. - -### Synopsis - -Edit an existing environment and initate a rebuild. - -``` -coder envs edit [flags] -``` - -### Examples - -``` -coder envs edit back-end-env --cpu 4 - -coder envs edit back-end-env --disk 20 -``` - -### Options - -``` - -c, --cpu float32 The number of cpu cores the environment should be provisioned with. - -d, --disk int The amount of disk storage an environment should be provisioned with. - --follow follow buildlog after initiating rebuild - --force force rebuild without showing a confirmation prompt - -g, --gpu int The amount of disk storage to provision the environment with. - -h, --help help for edit - -i, --image string name of the image you want the environment to be based off of. - -m, --memory float32 The amount of RAM an environment should be provisioned with. - -o, --org string name of the organization the environment should be created under. - -t, --tag string image tag of the image you want to base the environment off of. (default "latest") - --user string Specify the user whose resources to target (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder envs](coder_envs.md) - Interact with Coder environments - diff --git a/docs/coder_envs_rm.md b/docs/coder_envs_rm.md deleted file mode 100644 index e39acffd..00000000 --- a/docs/coder_envs_rm.md +++ /dev/null @@ -1,26 +0,0 @@ -## coder envs rm - -remove Coder environments by name - -``` -coder envs rm [...environment_names] [flags] -``` - -### Options - -``` - -f, --force force remove the specified environments without prompting first - -h, --help help for rm - --user string Specify the user whose resources to target (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder envs](coder_envs.md) - Interact with Coder environments - diff --git a/docs/coder_envs_stop.md b/docs/coder_envs_stop.md deleted file mode 100644 index 4fd38db2..00000000 --- a/docs/coder_envs_stop.md +++ /dev/null @@ -1,44 +0,0 @@ -## coder envs stop - -stop Coder environments by name - -### Synopsis - -Stop Coder environments by name - -``` -coder envs stop [...environment_names] [flags] -``` - -### Examples - -``` -coder envs stop front-end-env -coder envs stop front-end-env backend-env - -# stop all of your environments -coder envs ls -o json | jq -c '.[].name' | xargs coder envs stop - -# stop all environments for a given user -coder envs --user charlie@coder.com ls -o json \ - | jq -c '.[].name' \ - | xargs coder envs --user charlie@coder.com stop -``` - -### Options - -``` - -h, --help help for stop - --user string Specify the user whose resources to target (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder envs](coder_envs.md) - Interact with Coder environments - diff --git a/docs/coder_ssh.md b/docs/coder_ssh.md index 656b66fd..d57ac50e 100644 --- a/docs/coder_ssh.md +++ b/docs/coder_ssh.md @@ -1,9 +1,9 @@ ## coder ssh -Enter a shell of execute a command over SSH into a Coder environment +Enter a shell of execute a command over SSH into a Coder workspace ``` -coder ssh [environment_name] [] +coder ssh [workspace_name] [] ``` ### Examples diff --git a/docs/coder_sync.md b/docs/coder_sync.md index b872fab4..516f3160 100644 --- a/docs/coder_sync.md +++ b/docs/coder_sync.md @@ -1,9 +1,9 @@ ## coder sync -Establish a one way directory sync to a Coder environment +Establish a one way directory sync to a Coder workspace ``` -coder sync [local directory] [:] [flags] +coder sync [local directory] [:] [flags] ``` ### Options diff --git a/docs/coder_tokens.md b/docs/coder_tokens.md index 62bf7511..7b884e5b 100644 --- a/docs/coder_tokens.md +++ b/docs/coder_tokens.md @@ -5,7 +5,7 @@ manage Coder API tokens for the active user ### Synopsis Create and manage API Tokens for authenticating the CLI. -Statically authenticate using the token value with the `CODER_TOKEN` and `CODER_URL` environment variables. +Statically authenticate using the token value with the `CODER_TOKEN` and `CODER_URL` workspace variables. ### Options diff --git a/docs/coder_urls.md b/docs/coder_urls.md index efe016d1..2bb3e393 100644 --- a/docs/coder_urls.md +++ b/docs/coder_urls.md @@ -1,6 +1,6 @@ ## coder urls -Interact with environment DevURLs +Interact with workspace DevURLs ### Options @@ -18,6 +18,6 @@ Interact with environment DevURLs * [coder](coder.md) - coder provides a CLI for working with an existing Coder installation * [coder urls create](coder_urls_create.md) - Create a new dev URL for a workspace -* [coder urls ls](coder_urls_ls.md) - List all DevURLs for an environment +* [coder urls ls](coder_urls_ls.md) - List all DevURLs for a workspace * [coder urls rm](coder_urls_rm.md) - Remove a dev url diff --git a/docs/coder_urls_create.md b/docs/coder_urls_create.md index 81228aaa..eb3ed6fa 100644 --- a/docs/coder_urls_create.md +++ b/docs/coder_urls_create.md @@ -29,5 +29,5 @@ coder urls create my-workspace 8080 --name my-dev-url ### SEE ALSO -* [coder urls](coder_urls.md) - Interact with environment DevURLs +* [coder urls](coder_urls.md) - Interact with workspace DevURLs diff --git a/docs/coder_urls_ls.md b/docs/coder_urls_ls.md index 67d3fc2c..79048d9e 100644 --- a/docs/coder_urls_ls.md +++ b/docs/coder_urls_ls.md @@ -1,9 +1,9 @@ ## coder urls ls -List all DevURLs for an environment +List all DevURLs for a workspace ``` -coder urls ls [environment_name] [flags] +coder urls ls [workspace_name] [flags] ``` ### Options @@ -21,5 +21,5 @@ coder urls ls [environment_name] [flags] ### SEE ALSO -* [coder urls](coder_urls.md) - Interact with environment DevURLs +* [coder urls](coder_urls.md) - Interact with workspace DevURLs diff --git a/docs/coder_urls_rm.md b/docs/coder_urls_rm.md index be1f8e3c..5a25a3bf 100644 --- a/docs/coder_urls_rm.md +++ b/docs/coder_urls_rm.md @@ -3,7 +3,7 @@ Remove a dev url ``` -coder urls rm [environment_name] [port] [flags] +coder urls rm [workspace_name] [port] [flags] ``` ### Options @@ -20,5 +20,5 @@ coder urls rm [environment_name] [port] [flags] ### SEE ALSO -* [coder urls](coder_urls.md) - Interact with environment DevURLs +* [coder urls](coder_urls.md) - Interact with workspace DevURLs diff --git a/docs/coder_workspaces.md b/docs/coder_workspaces.md new file mode 100644 index 00000000..bb29bcf5 --- /dev/null +++ b/docs/coder_workspaces.md @@ -0,0 +1,32 @@ +## coder workspaces + +Interact with Coder workspaces + +### Synopsis + +Perform operations on the Coder workspaces owned by the active user. + +### Options + +``` + -h, --help help for workspaces +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation +* [coder workspaces create](coder_workspaces_create.md) - create a new workspace. +* [coder workspaces create-from-config](coder_workspaces_create-from-config.md) - create a new workspace from a template +* [coder workspaces edit](coder_workspaces_edit.md) - edit an existing workspace and initiate a rebuild. +* [coder workspaces ls](coder_workspaces_ls.md) - list all workspaces owned by the active user +* [coder workspaces rebuild](coder_workspaces_rebuild.md) - rebuild a Coder workspace +* [coder workspaces rm](coder_workspaces_rm.md) - remove Coder workspaces by name +* [coder workspaces stop](coder_workspaces_stop.md) - stop Coder workspaces by name +* [coder workspaces watch-build](coder_workspaces_watch-build.md) - trail the build log of a Coder workspace + diff --git a/docs/coder_workspaces_create-from-config.md b/docs/coder_workspaces_create-from-config.md new file mode 100644 index 00000000..3ad6c2ae --- /dev/null +++ b/docs/coder_workspaces_create-from-config.md @@ -0,0 +1,43 @@ +## coder workspaces create-from-config + +create a new workspace from a template + +### Synopsis + +Create a new Coder workspace using a Workspaces As Code template. + +``` +coder workspaces create-from-config [flags] +``` + +### Examples + +``` +# create a new workspace from git repository +coder workspaces create-from-config --name="dev-workspace" --repo-url https://github.com/cdr/m --ref my-branch +coder workspaces create-from-config --name="dev-workspace" -f coder.yaml +``` + +### Options + +``` + -f, --filepath string path to local template file. + --follow follow buildlog after initiating rebuild + -h, --help help for create-from-config + --name string name of the workspace to be created + -o, --org string name of the organization the workspace should be created under. + --provider string name of Workspace Provider with which to create the workspace + --ref string git reference to pull template from. May be a branch, tag, or commit hash. (default "master") + -r, --repo-url string URL of the git repository to pull the config from. Config file must live in '.coder/coder.yaml'. +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces + diff --git a/docs/coder_workspaces_create.md b/docs/coder_workspaces_create.md new file mode 100644 index 00000000..56732f89 --- /dev/null +++ b/docs/coder_workspaces_create.md @@ -0,0 +1,47 @@ +## coder workspaces create + +create a new workspace. + +### Synopsis + +Create a new Coder workspace. + +``` +coder workspaces create [workspace_name] [flags] +``` + +### Examples + +``` +# create a new workspace using default resource amounts +coder workspaces create my-new-workspace --image ubuntu +coder workspaces create my-new-powerful-workspace --cpu 12 --disk 100 --memory 16 --image ubuntu +``` + +### Options + +``` + --container-based-vm deploy the workspace as a Container-based VM + -c, --cpu float32 number of cpu cores the workspace should be provisioned with. + -d, --disk int GB of disk storage a workspace should be provisioned with. + --enable-autostart automatically start this workspace at your preferred time. + --follow follow buildlog after initiating rebuild + -g, --gpus int number GPUs a workspace should be provisioned with. + -h, --help help for create + -i, --image string name of the image to base the workspace off of. + -m, --memory float32 GB of RAM a workspace should be provisioned with. + -o, --org string name of the organization the workspace should be created under. + --provider string name of Workspace Provider with which to create the workspace + -t, --tag string tag of the image the workspace will be based off of. (default "latest") +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces + diff --git a/docs/coder_workspaces_edit.md b/docs/coder_workspaces_edit.md new file mode 100644 index 00000000..2d214cb4 --- /dev/null +++ b/docs/coder_workspaces_edit.md @@ -0,0 +1,46 @@ +## coder workspaces edit + +edit an existing workspace and initiate a rebuild. + +### Synopsis + +Edit an existing workspace and initate a rebuild. + +``` +coder workspaces edit [flags] +``` + +### Examples + +``` +coder workspaces edit back-end-workspace --cpu 4 + +coder workspaces edit back-end-workspace --disk 20 +``` + +### Options + +``` + -c, --cpu float32 The number of cpu cores the workspace should be provisioned with. + -d, --disk int The amount of disk storage a workspace should be provisioned with. + --follow follow buildlog after initiating rebuild + --force force rebuild without showing a confirmation prompt + -g, --gpu int The amount of disk storage to provision the workspace with. + -h, --help help for edit + -i, --image string name of the image you want the workspace to be based off of. + -m, --memory float32 The amount of RAM a workspace should be provisioned with. + -o, --org string name of the organization the workspace should be created under. + -t, --tag string image tag of the image you want to base the workspace off of. (default "latest") + --user string Specify the user whose resources to target (default "me") +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces + diff --git a/docs/coder_envs_ls.md b/docs/coder_workspaces_ls.md similarity index 51% rename from docs/coder_envs_ls.md rename to docs/coder_workspaces_ls.md index e43aec24..cd940e2b 100644 --- a/docs/coder_envs_ls.md +++ b/docs/coder_workspaces_ls.md @@ -1,13 +1,13 @@ -## coder envs ls +## coder workspaces ls -list all environments owned by the active user +list all workspaces owned by the active user ### Synopsis -List all Coder environments owned by the active user. +List all Coder workspaces owned by the active user. ``` -coder envs ls [flags] +coder workspaces ls [flags] ``` ### Options @@ -15,7 +15,7 @@ coder envs ls [flags] ``` -h, --help help for ls -o, --output string human | json (default "human") - -p, --provider string Filter environments by a particular workspace provider name. + -p, --provider string Filter workspaces by a particular workspace provider name. --user string Specify the user whose resources to target (default "me") ``` @@ -27,5 +27,5 @@ coder envs ls [flags] ### SEE ALSO -* [coder envs](coder_envs.md) - Interact with Coder environments +* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces diff --git a/docs/coder_envs_rebuild.md b/docs/coder_workspaces_rebuild.md similarity index 59% rename from docs/coder_envs_rebuild.md rename to docs/coder_workspaces_rebuild.md index a18d279e..8e928852 100644 --- a/docs/coder_envs_rebuild.md +++ b/docs/coder_workspaces_rebuild.md @@ -1,16 +1,16 @@ -## coder envs rebuild +## coder workspaces rebuild -rebuild a Coder environment +rebuild a Coder workspace ``` -coder envs rebuild [environment_name] [flags] +coder workspaces rebuild [workspace_name] [flags] ``` ### Examples ``` -coder envs rebuild front-end-env --follow -coder envs rebuild backend-env --force +coder workspaces rebuild front-end-workspace --follow +coder workspaces rebuild backend-workspace --force ``` ### Options @@ -30,5 +30,5 @@ coder envs rebuild backend-env --force ### SEE ALSO -* [coder envs](coder_envs.md) - Interact with Coder environments +* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces diff --git a/docs/coder_workspaces_rm.md b/docs/coder_workspaces_rm.md new file mode 100644 index 00000000..b19ece2d --- /dev/null +++ b/docs/coder_workspaces_rm.md @@ -0,0 +1,26 @@ +## coder workspaces rm + +remove Coder workspaces by name + +``` +coder workspaces rm [...workspace_names] [flags] +``` + +### Options + +``` + -f, --force force remove the specified workspaces without prompting first + -h, --help help for rm + --user string Specify the user whose resources to target (default "me") +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces + diff --git a/docs/coder_workspaces_stop.md b/docs/coder_workspaces_stop.md new file mode 100644 index 00000000..0e00ecef --- /dev/null +++ b/docs/coder_workspaces_stop.md @@ -0,0 +1,44 @@ +## coder workspaces stop + +stop Coder workspaces by name + +### Synopsis + +Stop Coder workspaces by name + +``` +coder workspaces stop [...workspace_names] [flags] +``` + +### Examples + +``` +coder workspaces stop front-end-workspace +coder workspaces stop front-end-workspace backend-workspace + +# stop all of your workspaces +coder workspaces ls -o json | jq -c '.[].name' | xargs coder workspaces stop + +# stop all workspaces for a given user +coder workspaces --user charlie@coder.com ls -o json \ + | jq -c '.[].name' \ + | xargs coder workspaces --user charlie@coder.com stop +``` + +### Options + +``` + -h, --help help for stop + --user string Specify the user whose resources to target (default "me") +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces + diff --git a/docs/coder_envs_watch-build.md b/docs/coder_workspaces_watch-build.md similarity index 53% rename from docs/coder_envs_watch-build.md rename to docs/coder_workspaces_watch-build.md index 19901fc5..8af34e82 100644 --- a/docs/coder_envs_watch-build.md +++ b/docs/coder_workspaces_watch-build.md @@ -1,15 +1,15 @@ -## coder envs watch-build +## coder workspaces watch-build -trail the build log of a Coder environment +trail the build log of a Coder workspace ``` -coder envs watch-build [environment_name] [flags] +coder workspaces watch-build [workspace_name] [flags] ``` ### Examples ``` -coder envs watch-build front-end-env +coder workspaces watch-build front-end-workspace ``` ### Options @@ -27,5 +27,5 @@ coder envs watch-build front-end-env ### SEE ALSO -* [coder envs](coder_envs.md) - Interact with Coder environments +* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces diff --git a/internal/activity/pusher.go b/internal/activity/pusher.go index f59630a1..f432c72e 100644 --- a/internal/activity/pusher.go +++ b/internal/activity/pusher.go @@ -16,19 +16,19 @@ const pushInterval = time.Minute // Pusher pushes activity metrics no more than once per pushInterval. Pushes // within the same interval are a no-op. type Pusher struct { - envID string - source string + workspaceID string + source string client coder.Client rate *rate.Limiter // Use a rate limiter to control the sampling rate. } // NewPusher instantiates a new instance of Pusher. -func NewPusher(c coder.Client, envID, source string) *Pusher { +func NewPusher(c coder.Client, workspaceID, source string) *Pusher { return &Pusher{ - envID: envID, - source: source, - client: c, + workspaceID: workspaceID, + source: source, + client: c, // Sample only 1 per interval to avoid spamming the api. rate: rate.NewLimiter(rate.Every(pushInterval), 1), } @@ -41,7 +41,7 @@ func (p *Pusher) Push(ctx context.Context) { return } - if err := p.client.PushActivity(ctx, p.source, p.envID); err != nil { + if err := p.client.PushActivity(ctx, p.source, p.workspaceID); err != nil { clog.Log(clog.Error(fmt.Sprintf("push activity: %s", err))) } } diff --git a/internal/cmd/ceapi.go b/internal/cmd/ceapi.go index 2e7b031d..c58b8161 100644 --- a/internal/cmd/ceapi.go +++ b/internal/cmd/ceapi.go @@ -32,8 +32,8 @@ func lookupUserOrgs(user *coder.User, orgs []coder.Organization) []coder.Organiz return userOrgs } -// getEnvs returns all environments for the user. -func getEnvs(ctx context.Context, client coder.Client, email string) ([]coder.Environment, error) { +// getWorkspaces returns all workspaces for the user. +func getWorkspaces(ctx context.Context, client coder.Client, email string) ([]coder.Workspace, error) { user, err := client.UserByEmail(ctx, email) if err != nil { return nil, xerrors.Errorf("get user: %w", err) @@ -46,51 +46,51 @@ func getEnvs(ctx context.Context, client coder.Client, email string) ([]coder.En orgs = lookupUserOrgs(user, orgs) - // NOTE: We don't know in advance how many envs we have so we can't pre-alloc. - var allEnvs []coder.Environment + // NOTE: We don't know in advance how many workspaces we have so we can't pre-alloc. + var allWorkspaces []coder.Workspace for _, org := range orgs { - envs, err := client.UserEnvironmentsByOrganization(ctx, user.ID, org.ID) + workspaces, err := client.UserWorkspacesByOrganization(ctx, user.ID, org.ID) if err != nil { - return nil, xerrors.Errorf("get envs for %s: %w", org.Name, err) + return nil, xerrors.Errorf("get workspaces for %s: %w", org.Name, err) } - allEnvs = append(allEnvs, envs...) + allWorkspaces = append(allWorkspaces, workspaces...) } - return allEnvs, nil + return allWorkspaces, nil } -// searchForEnv searches a user's environments to find the specified envName. If none is found, the haystack of -// environment names is returned. -func searchForEnv(ctx context.Context, client coder.Client, envName, userEmail string) (_ *coder.Environment, haystack []string, _ error) { - envs, err := getEnvs(ctx, client, userEmail) +// searchForWorkspace searches a user's workspaces to find the specified workspaceName. If none is found, the haystack of +// workspace names is returned. +func searchForWorkspace(ctx context.Context, client coder.Client, workspaceName, userEmail string) (_ *coder.Workspace, haystack []string, _ error) { + workspaces, err := getWorkspaces(ctx, client, userEmail) if err != nil { - return nil, nil, xerrors.Errorf("get environments: %w", err) + return nil, nil, xerrors.Errorf("get workspaces: %w", err) } - // NOTE: We don't know in advance where we will find the env, so we can't pre-alloc. - for _, env := range envs { - if env.Name == envName { - return &env, nil, nil + // NOTE: We don't know in advance where we will find the workspace, so we can't pre-alloc. + for _, workspace := range workspaces { + if workspace.Name == workspaceName { + return &workspace, nil, nil } // Keep track of what we found for the logs. - haystack = append(haystack, env.Name) + haystack = append(haystack, workspace.Name) } return nil, haystack, coder.ErrNotFound } -// findEnv returns a single environment by name (if it exists.). -func findEnv(ctx context.Context, client coder.Client, envName, userEmail string) (*coder.Environment, error) { - env, haystack, err := searchForEnv(ctx, client, envName, userEmail) +// findWorkspace returns a single workspace by name (if it exists.). +func findWorkspace(ctx context.Context, client coder.Client, workspaceName, userEmail string) (*coder.Workspace, error) { + workspace, haystack, err := searchForWorkspace(ctx, client, workspaceName, userEmail) if err != nil { return nil, clog.Fatal( - "failed to find environment", - fmt.Sprintf("environment %q not found in %q", envName, haystack), + "failed to find workspace", + fmt.Sprintf("workspace %q not found in %q", workspaceName, haystack), clog.BlankLine, - clog.Tipf("run \"coder envs ls\" to view your environments"), + clog.Tipf("run \"coder workspaces ls\" to view your workspaces"), ) } - return env, nil + return workspace, nil } type findImgConf struct { @@ -204,35 +204,35 @@ func getUserOrgs(ctx context.Context, client coder.Client, email string) ([]code return lookupUserOrgs(u, orgs), nil } -func getEnvsByProvider(ctx context.Context, client coder.Client, wpName, userEmail string) ([]coder.Environment, error) { +func getWorkspacesByProvider(ctx context.Context, client coder.Client, wpName, userEmail string) ([]coder.Workspace, error) { wp, err := coderutil.ProviderByName(ctx, client, wpName) if err != nil { return nil, err } - envs, err := client.EnvironmentsByWorkspaceProvider(ctx, wp.ID) + workspaces, err := client.WorkspacesByWorkspaceProvider(ctx, wp.ID) if err != nil { return nil, err } - envs, err = filterEnvsByUser(ctx, client, userEmail, envs) + workspaces, err = filterWorkspacesByUser(ctx, client, userEmail, workspaces) if err != nil { return nil, err } - return envs, nil + return workspaces, nil } -func filterEnvsByUser(ctx context.Context, client coder.Client, userEmail string, envs []coder.Environment) ([]coder.Environment, error) { +func filterWorkspacesByUser(ctx context.Context, client coder.Client, userEmail string, workspaces []coder.Workspace) ([]coder.Workspace, error) { user, err := client.UserByEmail(ctx, userEmail) if err != nil { return nil, xerrors.Errorf("get user: %w", err) } - var filteredEnvs []coder.Environment - for _, env := range envs { - if env.UserID == user.ID { - filteredEnvs = append(filteredEnvs, env) + var filteredWorkspaces []coder.Workspace + for _, workspace := range workspaces { + if workspace.UserID == user.ID { + filteredWorkspaces = append(filteredWorkspaces, workspace) } } - return filteredEnvs, nil + return filteredWorkspaces, nil } diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index be7b8f9e..38e5ae4f 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -28,7 +28,8 @@ func Make() *cobra.Command { usersCmd(), tagsCmd(), configSSHCmd(), - envsCmd(), + envCmd(), // DEPRECATED. + workspacesCmd(), syncCmd(), urlCmd(), tokensCmd(), @@ -83,7 +84,7 @@ MacOS: Zsh: -If shell completion is not already enabled in your environment you will need +If shell completion is not already enabled in your workspace you will need to enable it. You can execute the following once: $ echo "autoload -U compinit; compinit" >> ~/.zshrc diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index ee493660..9417c514 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -22,7 +22,7 @@ import ( const sshStartToken = "# ------------START-CODER-ENTERPRISE-----------" const sshStartMessage = `# The following has been auto-generated by "coder config-ssh" -# to make accessing your Coder environments easier. +# to make accessing your Coder workspaces easier. # # To remove this blob, run: # @@ -40,7 +40,7 @@ func configSSHCmd() *cobra.Command { cmd := &cobra.Command{ Use: "config-ssh", - Short: "Configure SSH to access Coder environments", + Short: "Configure SSH to access Coder workspaces", Long: "Inject the proper OpenSSH configuration into your local SSH config file.", RunE: configSSH(&configpath, &remove, &next), } @@ -98,21 +98,21 @@ func configSSH(configpath *string, remove *bool, next *bool) func(cmd *cobra.Com return xerrors.Errorf("fetch username: %w", err) } - envs, err := getEnvs(ctx, client, coder.Me) + workspaces, err := getWorkspaces(ctx, client, coder.Me) if err != nil { return err } - if len(envs) < 1 { - return xerrors.New("no environments found") + if len(workspaces) < 1 { + return xerrors.New("no workspaces found") } - envsWithProviders, err := coderutil.EnvsWithProvider(ctx, client, envs) + workspacesWithProviders, err := coderutil.WorkspacesWithProvider(ctx, client, workspaces) if err != nil { - return xerrors.Errorf("resolve env workspace providers: %w", err) + return xerrors.Errorf("resolve workspace workspace providers: %w", err) } - if !sshAvailable(envsWithProviders) { - return xerrors.New("SSH is disabled or not available for any environments in your Coder deployment.") + if !sshAvailable(workspacesWithProviders) { + return xerrors.New("SSH is disabled or not available for any workspaces in your Coder deployment.") } wconf, err := client.SiteConfigWorkspaces(ctx) @@ -132,7 +132,7 @@ func configSSH(configpath *string, remove *bool, next *bool) func(cmd *cobra.Com } } - newConfig := makeNewConfigs(user.Username, envsWithProviders, privateKeyFilepath, p2p) + newConfig := makeNewConfigs(user.Username, workspacesWithProviders, privateKeyFilepath, p2p) err = os.MkdirAll(filepath.Dir(*configpath), os.ModePerm) if err != nil { @@ -152,10 +152,10 @@ func configSSH(configpath *string, remove *bool, next *bool) func(cmd *cobra.Com fmt.Printf("Your private ssh key was written to \"%s\"\n", privateKeyFilepath) } - writeSSHUXState(ctx, client, user.ID, envs) + writeSSHUXState(ctx, client, user.ID, workspaces) fmt.Printf("An auto-generated ssh config was written to \"%s\"\n", *configpath) - 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.Println("You should now be able to ssh into your workspace") + fmt.Printf("For example, try running\n\n\t$ ssh coder.%s\n\n", workspaces[0].Name) return nil } } @@ -175,10 +175,10 @@ func removeOldConfig(config string) (string, bool) { return config[:startIndex-1] + config[endIndex+len(sshEndToken)+1:], true } -// sshAvailable returns true if SSH is available for at least one environment. -func sshAvailable(envs []coderutil.EnvWithWorkspaceProvider) bool { - for _, env := range envs { - if env.WorkspaceProvider.SSHEnabled { +// sshAvailable returns true if SSH is available for at least one workspace. +func sshAvailable(workspaces []coderutil.WorkspaceWithWorkspaceProvider) bool { + for _, workspace := range workspaces { + if workspace.WorkspaceProvider.SSHEnabled { return true } } @@ -193,34 +193,34 @@ func writeSSHKey(ctx context.Context, client coder.Client, privateKeyPath string return ioutil.WriteFile(privateKeyPath, []byte(key.PrivateKey), 0600) } -func makeNewConfigs(userName string, envs []coderutil.EnvWithWorkspaceProvider, privateKeyFilepath string, p2p bool) string { +func makeNewConfigs(userName string, workspaces []coderutil.WorkspaceWithWorkspaceProvider, privateKeyFilepath string, p2p bool) string { newConfig := fmt.Sprintf("\n%s\n%s\n\n", sshStartToken, sshStartMessage) - sort.Slice(envs, func(i, j int) bool { return envs[i].Env.Name < envs[j].Env.Name }) + sort.Slice(workspaces, func(i, j int) bool { return workspaces[i].Workspace.Name < workspaces[j].Workspace.Name }) - for _, env := range envs { - if !env.WorkspaceProvider.SSHEnabled { - clog.LogWarn(fmt.Sprintf("SSH is not enabled for workspace provider %q", env.WorkspaceProvider.Name), + for _, workspace := range workspaces { + if !workspace.WorkspaceProvider.SSHEnabled { + clog.LogWarn(fmt.Sprintf("SSH is not enabled for workspace provider %q", workspace.WorkspaceProvider.Name), clog.BlankLine, clog.Tipf("ask an infrastructure administrator to enable SSH for this workspace provider"), ) continue } - u, err := url.Parse(env.WorkspaceProvider.EnvproxyAccessURL) + u, err := url.Parse(workspace.WorkspaceProvider.EnvproxyAccessURL) if err != nil { - clog.LogWarn("invalid access url", clog.Causef("malformed url: %q", env.WorkspaceProvider.EnvproxyAccessURL)) + clog.LogWarn("invalid access url", clog.Causef("malformed url: %q", workspace.WorkspaceProvider.EnvproxyAccessURL)) continue } - useTunnel := env.WorkspaceProvider.BuiltIn && p2p - newConfig += makeSSHConfig(u.Host, userName, env.Env.Name, privateKeyFilepath, useTunnel) + useTunnel := workspace.WorkspaceProvider.BuiltIn && p2p + newConfig += makeSSHConfig(u.Host, userName, workspace.Workspace.Name, privateKeyFilepath, useTunnel) } newConfig += fmt.Sprintf("\n%s\n", sshEndToken) return newConfig } -func makeSSHConfig(host, userName, envName, privateKeyFilepath string, tunnel bool) string { +func makeSSHConfig(host, userName, workspaceName, privateKeyFilepath string, tunnel bool) string { if tunnel { return fmt.Sprintf( `Host coder.%s @@ -232,7 +232,7 @@ func makeSSHConfig(host, userName, envName, privateKeyFilepath string, tunnel bo IdentityFile="%s" ServerAliveInterval 60 ServerAliveCountMax 3 -`, envName, envName, envName, privateKeyFilepath) +`, workspaceName, workspaceName, workspaceName, privateKeyFilepath) } return fmt.Sprintf( @@ -245,7 +245,7 @@ func makeSSHConfig(host, userName, envName, privateKeyFilepath string, tunnel bo IdentityFile="%s" ServerAliveInterval 60 ServerAliveCountMax 3 -`, envName, host, userName, envName, privateKeyFilepath) +`, workspaceName, host, userName, workspaceName, privateKeyFilepath) } func writeStr(filename, data string) error { @@ -260,12 +260,12 @@ func readStr(filename string) (string, error) { return string(contents), nil } -func writeSSHUXState(ctx context.Context, client coder.Client, userID string, envs []coder.Environment) { - // Create a map of env.ID -> true to indicate to the web client that all - // current environments have SSH configured +func writeSSHUXState(ctx context.Context, client coder.Client, userID string, workspaces []coder.Workspace) { + // Create a map of workspace.ID -> true to indicate to the web client that all + // current workspaces have SSH configured cliSSHConfigured := make(map[string]bool) - for _, env := range envs { - cliSSHConfigured[env.ID] = true + for _, workspace := range workspaces { + cliSSHConfigured[workspace.ID] = true } // Update UXState that coder config-ssh has been run by the currently // authenticated user diff --git a/internal/cmd/login.go b/internal/cmd/login.go index a23cabfd..a706259c 100644 --- a/internal/cmd/login.go +++ b/internal/cmd/login.go @@ -47,11 +47,11 @@ func loginCmd() *cobra.Command { } } -// storeConfig writes the env URL and session token to the local config directory. +// storeConfig writes the workspace URL and session token to the local config directory. // The config lib will handle the local config path lookup and creation. -func storeConfig(envURL *url.URL, sessionToken string, urlCfg, sessionCfg config.File) error { - if err := urlCfg.Write(envURL.String()); err != nil { - return xerrors.Errorf("store env url: %w", err) +func storeConfig(workspaceURL *url.URL, sessionToken string, urlCfg, sessionCfg config.File) error { + if err := urlCfg.Write(workspaceURL.String()); err != nil { + return xerrors.Errorf("store workspace url: %w", err) } if err := sessionCfg.Write(sessionToken); err != nil { return xerrors.Errorf("store session token: %w", err) @@ -59,9 +59,9 @@ func storeConfig(envURL *url.URL, sessionToken string, urlCfg, sessionCfg config return nil } -func login(cmd *cobra.Command, envURL *url.URL) error { - authURL := *envURL - authURL.Path = envURL.Path + "/internal-auth" +func login(cmd *cobra.Command, workspaceURL *url.URL) error { + authURL := *workspaceURL + authURL.Path = workspaceURL.Path + "/internal-auth" q := authURL.Query() q.Add("show_token", "true") authURL.RawQuery = q.Encode() @@ -81,10 +81,10 @@ func login(cmd *cobra.Command, envURL *url.URL) error { return xerrors.Errorf("reading standard input: %w", err) } - if err := pingAPI(cmd.Context(), envURL, token); err != nil { + if err := pingAPI(cmd.Context(), workspaceURL, token); err != nil { return xerrors.Errorf("ping API with credentials: %w", err) } - if err := storeConfig(envURL, token, config.URL, config.Session); err != nil { + if err := storeConfig(workspaceURL, token, config.URL, config.Session); err != nil { return xerrors.Errorf("store auth: %w", err) } clog.LogSuccess("logged in") @@ -93,9 +93,9 @@ func login(cmd *cobra.Command, envURL *url.URL) error { // pingAPI creates a client from the given url/token and try to exec an api call. // Not using the SDK as we want to verify the url/token pair before storing the config files. -func pingAPI(ctx context.Context, envURL *url.URL, token string) error { +func pingAPI(ctx context.Context, workspaceURL *url.URL, token string) error { client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: envURL, + BaseURL: workspaceURL, Token: token, }) if err != nil { diff --git a/internal/cmd/rebuild.go b/internal/cmd/rebuild.go index 4df7d8e9..80a4521c 100644 --- a/internal/cmd/rebuild.go +++ b/internal/cmd/rebuild.go @@ -17,30 +17,30 @@ import ( "cdr.dev/coder-cli/pkg/clog" ) -func rebuildEnvCommand() *cobra.Command { +func rebuildWorkspaceCommand() *cobra.Command { var follow bool var force bool var user string cmd := &cobra.Command{ - Use: "rebuild [environment_name]", - Short: "rebuild a Coder environment", + Use: "rebuild [workspace_name]", + Short: "rebuild a Coder workspace", Args: xcobra.ExactArgs(1), - Example: `coder envs rebuild front-end-env --follow -coder envs rebuild backend-env --force`, + Example: `coder workspaces rebuild front-end-workspace --follow +coder workspaces rebuild backend-workspace --force`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() client, err := newClient(ctx, true) if err != nil { return err } - env, err := findEnv(ctx, client, args[0], user) + workspace, err := findWorkspace(ctx, client, args[0], user) if err != nil { return err } - if !force && env.LatestStat.ContainerStatus == coder.EnvironmentOn { + if !force && workspace.LatestStat.ContainerStatus == coder.WorkspaceOn { _, err = (&promptui.Prompt{ - Label: fmt.Sprintf("Rebuild environment %q? (will destroy any work outside of your home directory)", env.Name), + Label: fmt.Sprintf("Rebuild workspace %q? (will destroy any work outside of your home directory)", workspace.Name), IsConfirm: true, }).Run() if err != nil { @@ -51,17 +51,17 @@ coder envs rebuild backend-env --force`, } } - if err = client.RebuildEnvironment(ctx, env.ID); err != nil { + if err = client.RebuildWorkspace(ctx, workspace.ID); err != nil { return err } if follow { - if err = trailBuildLogs(ctx, client, env.ID); err != nil { + if err = trailBuildLogs(ctx, client, workspace.ID); err != nil { return err } } else { clog.LogSuccess( "successfully started rebuild", - clog.Tipf("run \"coder envs watch-build %s\" to follow the build logs", env.Name), + clog.Tipf("run \"coder workspaces watch-build %s\" to follow the build logs", workspace.Name), ) } return nil @@ -74,9 +74,9 @@ coder envs rebuild backend-env --force`, return cmd } -// trailBuildLogs follows the build log for a given environment and prints the staged +// trailBuildLogs follows the build log for a given workspace and prints the staged // output with loaders and success/failure indicators for each stage. -func trailBuildLogs(ctx context.Context, client coder.Client, envID string) error { +func trailBuildLogs(ctx context.Context, client coder.Client, workspaceID string) error { const check = "✅" const failure = "❌" @@ -85,7 +85,7 @@ func trailBuildLogs(ctx context.Context, client coder.Client, envID string) erro // this tells us whether to show dynamic loaders when printing output isTerminal := showInteractiveOutput - logs, err := client.FollowEnvironmentBuildLog(ctx, envID) + logs, err := client.FollowWorkspaceBuildLog(ctx, workspaceID) if err != nil { return err } @@ -160,9 +160,9 @@ func trailBuildLogs(ctx context.Context, client coder.Client, envID string) erro func watchBuildLogCommand() *cobra.Command { var user string cmd := &cobra.Command{ - Use: "watch-build [environment_name]", - Example: "coder envs watch-build front-end-env", - Short: "trail the build log of a Coder environment", + Use: "watch-build [workspace_name]", + Example: "coder workspaces watch-build front-end-workspace", + Short: "trail the build log of a Coder workspace", Args: xcobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -170,12 +170,12 @@ func watchBuildLogCommand() *cobra.Command { if err != nil { return err } - env, err := findEnv(ctx, client, args[0], user) + workspace, err := findWorkspace(ctx, client, args[0], user) if err != nil { return err } - if err = trailBuildLogs(ctx, client, env.ID); err != nil { + if err = trailBuildLogs(ctx, client, workspace.ID); err != nil { return err } return nil diff --git a/internal/cmd/resourcemanager.go b/internal/cmd/resourcemanager.go index 85d39c98..5be08d24 100644 --- a/internal/cmd/resourcemanager.go +++ b/internal/cmd/resourcemanager.go @@ -19,7 +19,7 @@ import ( func resourceCmd() *cobra.Command { cmd := &cobra.Command{ Use: "resources", - Short: "manage Coder resources with platform-level context (users, organizations, environments)", + Short: "manage Coder resources with platform-level context (users, organizations, workspaces)", Hidden: true, } cmd.AddCommand(resourceTop()) @@ -53,8 +53,8 @@ coder resources top --sort-by memory --show-empty`, cmd.Flags().StringVar(&options.user, "user", "", "filter by a user email") cmd.Flags().StringVar(&options.org, "org", "", "filter by the name of an organization") cmd.Flags().StringVar(&options.provider, "provider", "", "filter by the name of a workspace provider") - cmd.Flags().StringVar(&options.sortBy, "sort-by", "cpu", "field to sort aggregate groups and environments by (cpu|memory)") - cmd.Flags().BoolVar(&options.showEmptyGroups, "show-empty", false, "show groups with zero active environments") + cmd.Flags().StringVar(&options.sortBy, "sort-by", "cpu", "field to sort aggregate groups and workspaces by (cpu|memory)") + cmd.Flags().BoolVar(&options.showEmptyGroups, "show-empty", false, "show groups with zero active workspaces") return cmd } @@ -69,15 +69,15 @@ func runResourceTop(options *resourceTopOptions) func(cmd *cobra.Command, args [ // NOTE: it's not worth parrallelizing these calls yet given that this specific endpoint // takes about 20x times longer than the other two - allEnvs, err := client.Environments(ctx) + allWorkspaces, err := client.Workspaces(ctx) if err != nil { - return xerrors.Errorf("get environments %w", err) + return xerrors.Errorf("get workspaces %w", err) } - // only include environments whose last status was "ON" - envs := make([]coder.Environment, 0) - for _, e := range allEnvs { - if e.LatestStat.ContainerStatus == coder.EnvironmentOn { - envs = append(envs, e) + // only include workspaces whose last status was "ON" + workspaces := make([]coder.Workspace, 0) + for _, e := range allWorkspaces { + if e.LatestStat.ContainerStatus == coder.WorkspaceOn { + workspaces = append(workspaces, e) } } @@ -85,7 +85,7 @@ func runResourceTop(options *resourceTopOptions) func(cmd *cobra.Command, args [ if err != nil { return xerrors.Errorf("get users: %w", err) } - images, err := coderutil.MakeImageMap(ctx, client, envs) + images, err := coderutil.MakeImageMap(ctx, client, workspaces) if err != nil { return xerrors.Errorf("get images: %w", err) } @@ -100,11 +100,11 @@ func runResourceTop(options *resourceTopOptions) func(cmd *cobra.Command, args [ return xerrors.Errorf("get workspace providers: %w", err) } data := entities{ - providers: providers.Kubernetes, - users: users, - orgs: orgs, - envs: envs, - images: images, + providers: providers.Kubernetes, + users: users, + orgs: orgs, + workspaces: workspaces, + images: images, } return presentEntites(cmd.OutOrStdout(), data, *options) } @@ -113,7 +113,7 @@ func runResourceTop(options *resourceTopOptions) func(cmd *cobra.Command, args [ func presentEntites(w io.Writer, data entities, options resourceTopOptions) error { var ( groups []groupable - labeler envLabeler + labeler workspaceLabeler ) switch options.group { case "user": @@ -130,32 +130,32 @@ func presentEntites(w io.Writer, data entities, options resourceTopOptions) erro } type entities struct { - providers []coder.KubernetesProvider - users []coder.User - orgs []coder.Organization - envs []coder.Environment - images map[string]*coder.Image + providers []coder.KubernetesProvider + users []coder.User + orgs []coder.Organization + workspaces []coder.Workspace + images map[string]*coder.Image } -func aggregateByUser(data entities, options resourceTopOptions) ([]groupable, envLabeler) { +func aggregateByUser(data entities, options resourceTopOptions) ([]groupable, workspaceLabeler) { var groups []groupable providerIDMap := providerIDs(data.providers) orgIDMap := make(map[string]coder.Organization) for _, o := range data.orgs { orgIDMap[o.ID] = o } - userEnvs := make(map[string][]coder.Environment, len(data.users)) - for _, e := range data.envs { + userWorkspaces := make(map[string][]coder.Workspace, len(data.users)) + for _, e := range data.workspaces { if options.org != "" && orgIDMap[e.OrganizationID].Name != options.org { continue } - userEnvs[e.UserID] = append(userEnvs[e.UserID], e) + userWorkspaces[e.UserID] = append(userWorkspaces[e.UserID], e) } for _, u := range data.users { if options.user != "" && u.Email != options.user { continue } - groups = append(groups, userGrouping{user: u, envs: userEnvs[u.ID]}) + groups = append(groups, userGrouping{user: u, userWorkspaces: userWorkspaces[u.ID]}) } return groups, labelAll(imgLabeler(data.images), providerLabeler(providerIDMap), orgLabeler(orgIDMap)) } @@ -168,22 +168,22 @@ func userIDs(users []coder.User) map[string]coder.User { return userIDMap } -func aggregateByOrg(data entities, options resourceTopOptions) ([]groupable, envLabeler) { +func aggregateByOrg(data entities, options resourceTopOptions) ([]groupable, workspaceLabeler) { var groups []groupable providerIDMap := providerIDs(data.providers) - orgEnvs := make(map[string][]coder.Environment, len(data.orgs)) + orgWorkspaces := make(map[string][]coder.Workspace, len(data.orgs)) userIDMap := userIDs(data.users) - for _, e := range data.envs { + for _, e := range data.workspaces { if options.user != "" && userIDMap[e.UserID].Email != options.user { continue } - orgEnvs[e.OrganizationID] = append(orgEnvs[e.OrganizationID], e) + orgWorkspaces[e.OrganizationID] = append(orgWorkspaces[e.OrganizationID], e) } for _, o := range data.orgs { if options.org != "" && o.Name != options.org { continue } - groups = append(groups, orgGrouping{org: o, envs: orgEnvs[o.ID]}) + groups = append(groups, orgGrouping{org: o, orgWorkspaces: orgWorkspaces[o.ID]}) } return groups, labelAll(imgLabeler(data.images), userLabeler(userIDMap), providerLabeler(providerIDMap)) } @@ -196,39 +196,39 @@ func providerIDs(providers []coder.KubernetesProvider) map[string]coder.Kubernet return providerIDMap } -func aggregateByProvider(data entities, options resourceTopOptions) ([]groupable, envLabeler) { +func aggregateByProvider(data entities, options resourceTopOptions) ([]groupable, workspaceLabeler) { var groups []groupable providerIDMap := providerIDs(data.providers) userIDMap := userIDs(data.users) - providerEnvs := make(map[string][]coder.Environment, len(data.providers)) - for _, e := range data.envs { + providerWorkspaces := make(map[string][]coder.Workspace, len(data.providers)) + for _, e := range data.workspaces { if options.provider != "" && providerIDMap[e.ResourcePoolID].Name != options.provider { continue } - providerEnvs[e.ResourcePoolID] = append(providerEnvs[e.ResourcePoolID], e) + providerWorkspaces[e.ResourcePoolID] = append(providerWorkspaces[e.ResourcePoolID], e) } for _, p := range data.providers { if options.provider != "" && p.Name != options.provider { continue } - groups = append(groups, providerGrouping{provider: p, envs: providerEnvs[p.ID]}) + groups = append(groups, providerGrouping{provider: p, providerWorkspaces: providerWorkspaces[p.ID]}) } return groups, labelAll(imgLabeler(data.images), userLabeler(userIDMap)) // TODO: consider adding an org label here } -// groupable specifies a structure capable of being an aggregation group of environments (user, org, all). +// groupable specifies a structure capable of being an aggregation group of workspaces (user, org, all). type groupable interface { header() string - environments() []coder.Environment + workspaces() []coder.Workspace } type userGrouping struct { - user coder.User - envs []coder.Environment + user coder.User + userWorkspaces []coder.Workspace } -func (u userGrouping) environments() []coder.Environment { - return u.envs +func (u userGrouping) workspaces() []coder.Workspace { + return u.userWorkspaces } func (u userGrouping) header() string { @@ -236,12 +236,12 @@ func (u userGrouping) header() string { } type orgGrouping struct { - org coder.Organization - envs []coder.Environment + org coder.Organization + orgWorkspaces []coder.Workspace } -func (o orgGrouping) environments() []coder.Environment { - return o.envs +func (o orgGrouping) workspaces() []coder.Workspace { + return o.orgWorkspaces } func (o orgGrouping) header() string { @@ -253,29 +253,29 @@ func (o orgGrouping) header() string { } type providerGrouping struct { - provider coder.KubernetesProvider - envs []coder.Environment + provider coder.KubernetesProvider + providerWorkspaces []coder.Workspace } -func (p providerGrouping) environments() []coder.Environment { - return p.envs +func (p providerGrouping) workspaces() []coder.Workspace { + return p.providerWorkspaces } func (p providerGrouping) header() string { return fmt.Sprintf("%s\t", truncate(p.provider.Name, 20, "...")) } -func printResourceTop(writer io.Writer, groups []groupable, labeler envLabeler, showEmptyGroups bool, sortBy string) error { +func printResourceTop(writer io.Writer, groups []groupable, labeler workspaceLabeler, showEmptyGroups bool, sortBy string) error { tabwriter := tabwriter.NewWriter(writer, 0, 0, 4, ' ', 0) defer func() { _ = tabwriter.Flush() }() var userResources []aggregatedResources for _, group := range groups { - if !showEmptyGroups && len(group.environments()) < 1 { + if !showEmptyGroups && len(group.workspaces()) < 1 { continue } userResources = append(userResources, aggregatedResources{ - groupable: group, resources: aggregateEnvResources(group.environments()), + groupable: group, resources: aggregateWorkspaceResources(group.workspaces()), }) } @@ -287,19 +287,19 @@ func printResourceTop(writer io.Writer, groups []groupable, labeler envLabeler, for _, u := range userResources { _, _ = fmt.Fprintf(tabwriter, "%s\t%s", u.header(), u.resources) if verbose { - if len(u.environments()) > 0 { + if len(u.workspaces()) > 0 { _, _ = fmt.Fprintf(tabwriter, "\f") } - for _, env := range u.environments() { + for _, workspace := range u.workspaces() { _, _ = fmt.Fprintf(tabwriter, "\t") - _, _ = fmt.Fprintln(tabwriter, fmtEnvResources(env, labeler)) + _, _ = fmt.Fprintln(tabwriter, fmtWorkspaceResources(workspace, labeler)) } } _, _ = fmt.Fprint(tabwriter, "\n") } if len(userResources) == 0 { clog.LogInfo( - "no groups for the given filters exist with active environments", + "no groups for the given filters exist with active workspaces", clog.Tipf("run \"--show-empty\" to see groups with no resources."), ) } @@ -322,12 +322,12 @@ func sortAggregatedResources(resources []aggregatedResources, sortBy string) err return xerrors.Errorf("unknown --sort-by value of \"%s\"", sortBy) } for _, group := range resources { - envs := group.environments() + workspaces := group.workspaces() switch sortBy { case cpu: - sort.Slice(envs, func(i, j int) bool { return envs[i].CPUCores > envs[j].CPUCores }) + sort.Slice(workspaces, func(i, j int) bool { return workspaces[i].CPUCores > workspaces[j].CPUCores }) case memory: - sort.Slice(envs, func(i, j int) bool { return envs[i].MemoryGB > envs[j].MemoryGB }) + sort.Slice(workspaces, func(i, j int) bool { return workspaces[i].MemoryGB > workspaces[j].MemoryGB }) default: return xerrors.Errorf("unknown --sort-by value of \"%s\"", sortBy) } @@ -340,28 +340,28 @@ type aggregatedResources struct { resources } -func resourcesFromEnv(env coder.Environment) resources { +func resourcesFromWorkspace(workspace coder.Workspace) resources { return resources{ - cpuAllocation: env.CPUCores, - cpuUtilization: env.LatestStat.CPUUsage, - memAllocation: env.MemoryGB, - memUtilization: env.LatestStat.MemoryUsage, + cpuAllocation: workspace.CPUCores, + cpuUtilization: workspace.LatestStat.CPUUsage, + memAllocation: workspace.MemoryGB, + memUtilization: workspace.LatestStat.MemoryUsage, } } -func fmtEnvResources(env coder.Environment, labeler envLabeler) string { - return fmt.Sprintf("%s\t%s\t%s", truncate(env.Name, 20, "..."), resourcesFromEnv(env), labeler.label(env)) +func fmtWorkspaceResources(workspace coder.Workspace, labeler workspaceLabeler) string { + return fmt.Sprintf("%s\t%s\t%s", truncate(workspace.Name, 20, "..."), resourcesFromWorkspace(workspace), labeler.label(workspace)) } -type envLabeler interface { - label(coder.Environment) string +type workspaceLabeler interface { + label(coder.Workspace) string } -func labelAll(labels ...envLabeler) envLabeler { return multiLabeler(labels) } +func labelAll(labels ...workspaceLabeler) workspaceLabeler { return multiLabeler(labels) } -type multiLabeler []envLabeler +type multiLabeler []workspaceLabeler -func (m multiLabeler) label(e coder.Environment) string { +func (m multiLabeler) label(e coder.Workspace) string { var str strings.Builder for i, labeler := range m { if i != 0 { @@ -374,31 +374,31 @@ func (m multiLabeler) label(e coder.Environment) string { type orgLabeler map[string]coder.Organization -func (o orgLabeler) label(e coder.Environment) string { +func (o orgLabeler) label(e coder.Workspace) string { return fmt.Sprintf("[org: %s]", o[e.OrganizationID].Name) } type imgLabeler map[string]*coder.Image -func (i imgLabeler) label(e coder.Environment) string { +func (i imgLabeler) label(e coder.Workspace) string { return fmt.Sprintf("[img: %s:%s]", i[e.ImageID].Repository, e.ImageTag) } type userLabeler map[string]coder.User -func (u userLabeler) label(e coder.Environment) string { +func (u userLabeler) label(e coder.Workspace) string { return fmt.Sprintf("[user: %s]", u[e.UserID].Email) } type providerLabeler map[string]coder.KubernetesProvider -func (p providerLabeler) label(e coder.Environment) string { +func (p providerLabeler) label(e coder.Workspace) string { return fmt.Sprintf("[provider: %s]", p[e.ResourcePoolID].Name) } -func aggregateEnvResources(envs []coder.Environment) resources { +func aggregateWorkspaceResources(workspaces []coder.Workspace) resources { var aggregate resources - for _, e := range envs { + for _, e := range workspaces { aggregate.cpuAllocation += e.CPUCores aggregate.cpuUtilization += e.LatestStat.CPUUsage aggregate.memAllocation += e.MemoryGB diff --git a/internal/cmd/resourcemanager_test.go b/internal/cmd/resourcemanager_test.go index 50c9156c..ffe3dbb9 100644 --- a/internal/cmd/resourcemanager_test.go +++ b/internal/cmd/resourcemanager_test.go @@ -85,7 +85,7 @@ func mockResourceTopEntities() entities { imageIDs := [...]string{randString(10), randString(10), randString(10)} providerIDs := [...]string{randString(10), randString(10), randString(10)} userIDs := [...]string{randString(10), randString(10), randString(10)} - envIDs := [...]string{randString(10), randString(10), randString(10), randString(10)} + workspaceIDs := [...]string{randString(10), randString(10), randString(10), randString(10)} return entities{ providers: []coder.KubernetesProvider{ @@ -126,47 +126,47 @@ func mockResourceTopEntities() entities { Members: []coder.OrganizationUser{{}, {}}, }, }, - envs: []coder.Environment{ + workspaces: []coder.Workspace{ { - ID: envIDs[0], + ID: workspaceIDs[0], ResourcePoolID: providerIDs[0], ImageID: imageIDs[0], OrganizationID: orgIDs[0], UserID: userIDs[0], - Name: "dev-env", + Name: "dev-workspace", ImageTag: "20.04", CPUCores: 12.2, MemoryGB: 64.4, - LatestStat: coder.EnvironmentStat{ - ContainerStatus: coder.EnvironmentOn, + LatestStat: coder.WorkspaceStat{ + ContainerStatus: coder.WorkspaceOn, }, }, { - ID: envIDs[1], + ID: workspaceIDs[1], ResourcePoolID: providerIDs[1], ImageID: imageIDs[1], OrganizationID: orgIDs[1], UserID: userIDs[1], - Name: "another-env", + Name: "another-workspace", ImageTag: "10.2", CPUCores: 4, MemoryGB: 16, - LatestStat: coder.EnvironmentStat{ - ContainerStatus: coder.EnvironmentOn, + LatestStat: coder.WorkspaceStat{ + ContainerStatus: coder.WorkspaceOn, }, }, { - ID: envIDs[2], + ID: workspaceIDs[2], ResourcePoolID: providerIDs[1], ImageID: imageIDs[1], OrganizationID: orgIDs[1], UserID: userIDs[1], - Name: "yet-another-env", + Name: "yet-another-workspace", ImageTag: "10.2", CPUCores: 100, MemoryGB: 2, - LatestStat: coder.EnvironmentStat{ - ContainerStatus: coder.EnvironmentOn, + LatestStat: coder.WorkspaceStat{ + ContainerStatus: coder.WorkspaceOn, }, }, }, diff --git a/internal/cmd/resourcemanager_test.golden b/internal/cmd/resourcemanager_test.golden index 0707bd1a..0bed13ee 100755 --- a/internal/cmd/resourcemanager_test.golden +++ b/internal/cmd/resourcemanager_test.golden @@ -1,32 +1,32 @@ === TEST: By User Second Random (second-random@coder.com) [cpu: 104.0] [mem: 18.0 GB] - yet-another-env [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] - another-env [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] + yet-another-workspace... [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] + another-workspace [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] Random (random@coder.com) [cpu: 12.2] [mem: 64.4 GB] - dev-env [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [provider: mars] [org: SpecialOrg] + dev-workspace [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [provider: mars] [org: SpecialOrg] === TEST: By Org NotSoSpecialOrg (2 members) [cpu: 104.0] [mem: 18.0 GB] - yet-another-env [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] [provider: underground] - another-env [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] [provider: underground] + yet-another-workspace... [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] [provider: underground] + another-workspace [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] [provider: underground] SpecialOrg (2 members) [cpu: 12.2] [mem: 64.4 GB] - dev-env [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [user: random@coder.com] [provider: mars] + dev-workspace [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [user: random@coder.com] [provider: mars] === TEST: By Provider underground [cpu: 104.0] [mem: 18.0 GB] - yet-another-env [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] - another-env [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] + yet-another-workspace... [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] + another-workspace [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] mars [cpu: 12.2] [mem: 64.4 GB] - dev-env [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [user: random@coder.com] + dev-workspace [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [user: random@coder.com] === TEST: Sort By Memory Random (random@coder.com) [cpu: 12.2] [mem: 64.4 GB] - dev-env [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [provider: mars] [org: SpecialOrg] + dev-workspace [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [provider: mars] [org: SpecialOrg] Second Random (second-random@coder.com) [cpu: 104.0] [mem: 18.0 GB] - another-env [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] - yet-another-env [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] + another-workspace [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] + yet-another-workspace... [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] diff --git a/internal/cmd/ssh.go b/internal/cmd/ssh.go index 34478e0a..cec588a6 100644 --- a/internal/cmd/ssh.go +++ b/internal/cmd/ssh.go @@ -22,8 +22,8 @@ var ( func sshCmd() *cobra.Command { cmd := cobra.Command{ - Use: "ssh [environment_name] []", - Short: "Enter a shell of execute a command over SSH into a Coder environment", + Use: "ssh [workspace_name] []", + Short: "Enter a shell of execute a command over SSH into a Coder workspace", Args: shValidArgs, Example: `coder ssh my-dev coder ssh my-dev pwd`, @@ -45,18 +45,18 @@ func shell(cmd *cobra.Command, args []string) error { if err != nil { return err } - env, err := findEnv(ctx, client, args[0], coder.Me) + workspace, err := findWorkspace(ctx, client, args[0], coder.Me) if err != nil { return err } - if env.LatestStat.ContainerStatus != coder.EnvironmentOn { - return clog.Error("environment not available", - fmt.Sprintf("current status: \"%s\"", env.LatestStat.ContainerStatus), + if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { + return clog.Error("workspace not available", + fmt.Sprintf("current status: \"%s\"", workspace.LatestStat.ContainerStatus), clog.BlankLine, - clog.Tipf("use \"coder envs rebuild %s\" to rebuild this environment", env.Name), + clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name), ) } - wp, err := client.WorkspaceProviderByID(ctx, env.ResourcePoolID) + wp, err := client.WorkspaceProviderByID(ctx, workspace.ResourcePoolID) if err != nil { return err } @@ -77,7 +77,7 @@ func shell(cmd *cobra.Command, args []string) error { } ssh := exec.CommandContext(ctx, "ssh", "-i"+privateKeyFilepath, - fmt.Sprintf("%s-%s@%s", me.Username, env.Name, u.Hostname()), + fmt.Sprintf("%s-%s@%s", me.Username, workspace.Name, u.Hostname()), ) if len(args) > 1 { ssh.Args = append(ssh.Args, args[1:]...) @@ -101,17 +101,17 @@ func shValidArgs(cmd *cobra.Command, args []string) error { if err != nil { client, err := newClient(ctx, true) if err != nil { - return clog.Error("missing [environment_name] argument") + return clog.Error("missing [workspace_name] argument") } - _, haystack, err := searchForEnv(ctx, client, "", coder.Me) + _, haystack, err := searchForWorkspace(ctx, client, "", coder.Me) if err != nil { - return clog.Error("missing [environment_name] argument", + return clog.Error("missing [workspace_name] argument", fmt.Sprintf("specify one of %q", haystack), clog.BlankLine, - clog.Tipf("run \"coder envs ls\" to view your environments"), + clog.Tipf("run \"coder workspaces ls\" to view your workspaces"), ) } - return clog.Error("missing [environment_name] argument") + return clog.Error("missing [workspace_name] argument") } return nil } diff --git a/internal/cmd/sync.go b/internal/cmd/sync.go index 922f2679..eadb3853 100644 --- a/internal/cmd/sync.go +++ b/internal/cmd/sync.go @@ -20,8 +20,8 @@ import ( func syncCmd() *cobra.Command { var init bool cmd := &cobra.Command{ - Use: "sync [local directory] [:]", - Short: "Establish a one way directory sync to a Coder environment", + Use: "sync [local directory] [:]", + Short: "Establish a one way directory sync to a Coder workspace", Args: xcobra.ExactArgs(2), RunE: makeRunSync(&init), } @@ -64,11 +64,11 @@ func makeRunSync(init *bool) func(cmd *cobra.Command, args []string) error { return xerrors.New("remote malformatted") } var ( - envName = remoteTokens[0] - remoteDir = remoteTokens[1] + workspaceName = remoteTokens[0] + remoteDir = remoteTokens[1] ) - env, err := findEnv(ctx, client, envName, coder.Me) + workspace, err := findWorkspace(ctx, client, workspaceName, coder.Me) if err != nil { return err } @@ -78,7 +78,7 @@ func makeRunSync(init *bool) func(cmd *cobra.Command, args []string) error { return err } if info.Mode().IsRegular() { - return sync.SingleFile(ctx, local, remoteDir, env, client) + return sync.SingleFile(ctx, local, remoteDir, workspace, client) } if !info.IsDir() { return xerrors.Errorf("local path must lead to a regular file or directory: %w", err) @@ -91,7 +91,7 @@ func makeRunSync(init *bool) func(cmd *cobra.Command, args []string) error { s := sync.Sync{ Init: *init, - Env: *env, + Workspace: *workspace, RemoteDir: remoteDir, LocalDir: absLocal, Client: client, diff --git a/internal/cmd/tags.go b/internal/cmd/tags.go index 201133b0..24a7affa 100644 --- a/internal/cmd/tags.go +++ b/internal/cmd/tags.go @@ -36,7 +36,7 @@ func tagsCreateCmd() *cobra.Command { cmd := &cobra.Command{ Use: "create [tag]", Short: "add an image tag", - Long: "allow users to create environments with this image tag", + Long: "allow users to create workspaces with this image tag", Example: `coder tags create latest --image ubuntu --org default`, Args: xcobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/internal/cmd/tokens.go b/internal/cmd/tokens.go index 0763a5b0..a014f546 100644 --- a/internal/cmd/tokens.go +++ b/internal/cmd/tokens.go @@ -17,7 +17,7 @@ func tokensCmd() *cobra.Command { Use: "tokens", Short: "manage Coder API tokens for the active user", Long: "Create and manage API Tokens for authenticating the CLI.\n" + - "Statically authenticate using the token value with the " + "`" + "CODER_TOKEN" + "`" + " and " + "`" + "CODER_URL" + "`" + " environment variables.", + "Statically authenticate using the token value with the " + "`" + "CODER_TOKEN" + "`" + " and " + "`" + "CODER_URL" + "`" + " workspace variables.", } cmd.AddCommand( lsTokensCmd(), diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index 09463589..ae7fe14c 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -55,19 +55,19 @@ coder tunnel my-dev 3000 3000 } baseURL := sdk.BaseURL() - envs, err := getEnvs(ctx, sdk, coder.Me) + workspaces, err := getWorkspaces(ctx, sdk, coder.Me) if err != nil { return xerrors.Errorf("get workspaces: %w", err) } - var envID string - for _, env := range envs { - if env.Name == args[0] { - envID = env.ID + var workspaceID string + for _, workspace := range workspaces { + if workspace.Name == args[0] { + workspaceID = workspace.ID break } } - if envID == "" { + if workspaceID == "" { return xerrors.Errorf("No workspace found by name '%s'", args[0]) } @@ -75,7 +75,7 @@ coder tunnel my-dev 3000 3000 log: log, brokerAddr: &baseURL, token: sdk.Token(), - workspaceID: envID, + workspaceID: workspaceID, stdio: args[2] == "stdio", localPort: uint16(localPort), remotePort: uint16(remotePort), diff --git a/internal/cmd/urls.go b/internal/cmd/urls.go index 64836229..b6730d34 100644 --- a/internal/cmd/urls.go +++ b/internal/cmd/urls.go @@ -21,18 +21,18 @@ func urlCmd() *cobra.Command { var outputFmt string cmd := &cobra.Command{ Use: "urls", - Short: "Interact with environment DevURLs", + Short: "Interact with workspace DevURLs", } lsCmd := &cobra.Command{ - Use: "ls [environment_name]", - Short: "List all DevURLs for an environment", + Use: "ls [workspace_name]", + Short: "List all DevURLs for a workspace", Args: xcobra.ExactArgs(1), RunE: listDevURLsCmd(&outputFmt), } lsCmd.Flags().StringVarP(&outputFmt, "output", "o", humanOutput, "human|json") rmCmd := &cobra.Command{ - Use: "rm [environment_name] [port]", + Use: "rm [workspace_name] [port]", Args: cobra.ExactArgs(2), Short: "Remove a dev url", RunE: removeDevURL, @@ -77,7 +77,7 @@ func accessLevelIsValid(level string) bool { } // Run gets the list of active devURLs from the cemanager for the -// specified environment and outputs info to stdout. +// specified workspace and outputs info to stdout. func listDevURLsCmd(outputFmt *string) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -85,9 +85,9 @@ func listDevURLsCmd(outputFmt *string) func(cmd *cobra.Command, args []string) e if err != nil { return err } - envName := args[0] + workspaceName := args[0] - devURLs, err := urlList(ctx, client, envName) + devURLs, err := urlList(ctx, client, workspaceName) if err != nil { return err } @@ -95,7 +95,7 @@ func listDevURLsCmd(outputFmt *string) func(cmd *cobra.Command, args []string) e switch *outputFmt { case humanOutput: if len(devURLs) < 1 { - clog.LogInfo(fmt.Sprintf("no devURLs found for environment %q", envName)) + clog.LogInfo(fmt.Sprintf("no devURLs found for workspace %q", workspaceName)) return nil } err := tablewriter.WriteTable(cmd.OutOrStdout(), len(devURLs), func(i int) interface{} { @@ -129,9 +129,9 @@ func createDevURLCmd() *cobra.Command { Example: `coder urls create my-workspace 8080 --name my-dev-url`, RunE: func(cmd *cobra.Command, args []string) error { var ( - envName = args[0] - port = args[1] - ctx = cmd.Context() + workspaceName = args[0] + port = args[1] + ctx = cmd.Context() ) portNum, err := validatePort(port) @@ -152,36 +152,36 @@ func createDevURLCmd() *cobra.Command { return err } - env, err := findEnv(ctx, client, envName, coder.Me) + workspace, err := findWorkspace(ctx, client, workspaceName, coder.Me) if err != nil { return err } - urls, err := urlList(ctx, client, envName) + urls, err := urlList(ctx, client, workspaceName) if err != nil { return err } urlID, found := devURLID(portNum, urls) if found { - err := client.PutDevURL(ctx, env.ID, urlID, coder.PutDevURLReq{ - Port: portNum, - Name: urlname, - Access: access, - EnvID: env.ID, - Scheme: scheme, + err := client.PutDevURL(ctx, workspace.ID, urlID, coder.PutDevURLReq{ + Port: portNum, + Name: urlname, + Access: access, + WorkspaceID: workspace.ID, + Scheme: scheme, }) if err != nil { return xerrors.Errorf("update DevURL: %w", err) } clog.LogSuccess(fmt.Sprintf("patched devurl for port %s", port)) } else { - err := client.CreateDevURL(ctx, env.ID, coder.CreateDevURLReq{ - Port: portNum, - Name: urlname, - Access: access, - EnvID: env.ID, - Scheme: scheme, + err := client.CreateDevURL(ctx, workspace.ID, coder.CreateDevURLReq{ + Port: portNum, + Name: urlname, + Access: access, + WorkspaceID: workspace.ID, + Scheme: scheme, }) if err != nil { return xerrors.Errorf("insert DevURL: %w", err) @@ -203,7 +203,7 @@ func createDevURLCmd() *cobra.Command { // consist solely of letters and digits, with a max length of 64 chars. var devURLNameValidRx = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9]{0,63}$") -// devURLID returns the ID of a devURL, given the env name and port +// devURLID returns the ID of a devURL, given the workspace name and port // from a list of DevURL records. // ("", false) is returned if no match is found. func devURLID(port int, urls []coder.DevURL) (string, bool) { @@ -215,12 +215,12 @@ func devURLID(port int, urls []coder.DevURL) (string, bool) { return "", false } -// Run deletes a devURL, specified by env ID and port, from the cemanager. +// Run deletes a devURL, specified by workspace ID and port, from the cemanager. func removeDevURL(cmd *cobra.Command, args []string) error { var ( - envName = args[0] - port = args[1] - ctx = cmd.Context() + workspaceName = args[0] + port = args[1] + ctx = cmd.Context() ) portNum, err := validatePort(port) @@ -232,12 +232,12 @@ func removeDevURL(cmd *cobra.Command, args []string) error { if err != nil { return err } - env, err := findEnv(ctx, client, envName, coder.Me) + workspace, err := findWorkspace(ctx, client, workspaceName, coder.Me) if err != nil { return err } - urls, err := urlList(ctx, client, envName) + urls, err := urlList(ctx, client, workspaceName) if err != nil { return err } @@ -249,17 +249,17 @@ func removeDevURL(cmd *cobra.Command, args []string) error { return xerrors.Errorf("No devurl found for port %v", port) } - if err := client.DeleteDevURL(ctx, env.ID, urlID); err != nil { + if err := client.DeleteDevURL(ctx, workspace.ID, urlID); err != nil { return xerrors.Errorf("delete DevURL: %w", err) } return nil } // urlList returns the list of active devURLs from the cemanager. -func urlList(ctx context.Context, client coder.Client, envName string) ([]coder.DevURL, error) { - env, err := findEnv(ctx, client, envName, coder.Me) +func urlList(ctx context.Context, client coder.Client, workspaceName string) ([]coder.DevURL, error) { + workspace, err := findWorkspace(ctx, client, workspaceName, coder.Me) if err != nil { return nil, err } - return client.DevURLs(ctx, env.ID) + return client.DevURLs(ctx, workspace.ID) } diff --git a/internal/cmd/envs.go b/internal/cmd/workspaces.go similarity index 60% rename from internal/cmd/envs.go rename to internal/cmd/workspaces.go index 352b2567..2660dc3e 100644 --- a/internal/cmd/envs.go +++ b/internal/cmd/workspaces.go @@ -21,22 +21,31 @@ import ( const defaultImgTag = "latest" -func envsCmd() *cobra.Command { +func envCmd() *cobra.Command { + cmd := workspacesCmd() + cmd.Use = "envs" + cmd.Deprecated = "use \"workspaces\" instead" + cmd.Aliases = []string{} + return cmd +} + +func workspacesCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "envs", - Short: "Interact with Coder environments", - Long: "Perform operations on the Coder environments owned by the active user.", + Use: "workspaces", + Short: "Interact with Coder workspaces", + Long: "Perform operations on the Coder workspaces owned by the active user.", + Aliases: []string{"ws"}, } cmd.AddCommand( - lsEnvsCommand(), - stopEnvsCmd(), - rmEnvsCmd(), + lsWorkspacesCommand(), + stopWorkspacesCmd(), + rmWorkspacesCmd(), watchBuildLogCommand(), - rebuildEnvCommand(), - createEnvCmd(), - createEnvFromConfigCmd(), - editEnvCmd(), + rebuildWorkspaceCommand(), + createWorkspaceCmd(), + createWorkspaceFromConfigCmd(), + editWorkspaceCmd(), ) return cmd } @@ -46,7 +55,7 @@ const ( jsonOutput = "json" ) -func lsEnvsCommand() *cobra.Command { +func lsWorkspacesCommand() *cobra.Command { var ( outputFmt string user string @@ -55,45 +64,45 @@ func lsEnvsCommand() *cobra.Command { cmd := &cobra.Command{ Use: "ls", - Short: "list all environments owned by the active user", - Long: "List all Coder environments owned by the active user.", + Short: "list all workspaces owned by the active user", + Long: "List all Coder workspaces owned by the active user.", RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() client, err := newClient(ctx, true) if err != nil { return err } - envs, err := getEnvs(ctx, client, user) + workspaces, err := getWorkspaces(ctx, client, user) if err != nil { return err } if provider != "" { - envs, err = getEnvsByProvider(ctx, client, provider, user) + workspaces, err = getWorkspacesByProvider(ctx, client, provider, user) if err != nil { return err } } - if len(envs) < 1 { - clog.LogInfo("no environments found") - envs = []coder.Environment{} // ensures that json output still marshals + if len(workspaces) < 1 { + clog.LogInfo("no workspaces found") + workspaces = []coder.Workspace{} // ensures that json output still marshals } switch outputFmt { case humanOutput: - envs, err := coderutil.EnvsHumanTable(ctx, client, envs) + workspaces, err := coderutil.WorkspacesHumanTable(ctx, client, workspaces) if err != nil { return err } - err = tablewriter.WriteTable(cmd.OutOrStdout(), len(envs), func(i int) interface{} { - return envs[i] + err = tablewriter.WriteTable(cmd.OutOrStdout(), len(workspaces), func(i int) interface{} { + return workspaces[i] }) if err != nil { return xerrors.Errorf("write table: %w", err) } case jsonOutput: - err := json.NewEncoder(cmd.OutOrStdout()).Encode(envs) + err := json.NewEncoder(cmd.OutOrStdout()).Encode(workspaces) if err != nil { - return xerrors.Errorf("write environments as JSON: %w", err) + return xerrors.Errorf("write workspaces as JSON: %w", err) } default: return xerrors.Errorf("unknown --output value %q", outputFmt) @@ -104,27 +113,27 @@ func lsEnvsCommand() *cobra.Command { cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") cmd.Flags().StringVarP(&outputFmt, "output", "o", humanOutput, "human | json") - cmd.Flags().StringVarP(&provider, "provider", "p", "", "Filter environments by a particular workspace provider name.") + cmd.Flags().StringVarP(&provider, "provider", "p", "", "Filter workspaces by a particular workspace provider name.") return cmd } -func stopEnvsCmd() *cobra.Command { +func stopWorkspacesCmd() *cobra.Command { var user string cmd := &cobra.Command{ - Use: "stop [...environment_names]", - Short: "stop Coder environments by name", - Long: "Stop Coder environments by name", - Example: `coder envs stop front-end-env -coder envs stop front-end-env backend-env + Use: "stop [...workspace_names]", + Short: "stop Coder workspaces by name", + Long: "Stop Coder workspaces by name", + Example: `coder workspaces stop front-end-workspace +coder workspaces stop front-end-workspace backend-workspace -# stop all of your environments -coder envs ls -o json | jq -c '.[].name' | xargs coder envs stop +# stop all of your workspaces +coder workspaces ls -o json | jq -c '.[].name' | xargs coder workspaces stop -# stop all environments for a given user -coder envs --user charlie@coder.com ls -o json \ +# stop all workspaces for a given user +coder workspaces --user charlie@coder.com ls -o json \ | jq -c '.[].name' \ - | xargs coder envs --user charlie@coder.com stop`, + | xargs coder workspaces --user charlie@coder.com stop`, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -134,21 +143,21 @@ coder envs --user charlie@coder.com ls -o json \ } egroup := clog.LoggedErrGroup() - for _, envName := range args { - envName := envName + for _, workspaceName := range args { + workspaceName := workspaceName egroup.Go(func() error { - env, err := findEnv(ctx, client, envName, user) + workspace, err := findWorkspace(ctx, client, workspaceName, user) if err != nil { return err } - if err = client.StopEnvironment(ctx, env.ID); err != nil { - return clog.Error(fmt.Sprintf("stop environment %q", env.Name), + if err = client.StopWorkspace(ctx, workspace.ID); err != nil { + return clog.Error(fmt.Sprintf("stop workspace %q", workspace.Name), clog.Causef(err.Error()), clog.BlankLine, - clog.Hintf("current environment status is %q", env.LatestStat.ContainerStatus), + clog.Hintf("current workspace status is %q", workspace.LatestStat.ContainerStatus), ) } - clog.LogSuccess(fmt.Sprintf("successfully stopped environment %q", envName)) + clog.LogSuccess(fmt.Sprintf("successfully stopped workspace %q", workspaceName)) return nil }) } @@ -160,7 +169,7 @@ coder envs --user charlie@coder.com ls -o json \ return cmd } -func createEnvCmd() *cobra.Command { +func createWorkspaceCmd() *cobra.Command { var ( org string cpu float32 @@ -176,13 +185,13 @@ func createEnvCmd() *cobra.Command { ) cmd := &cobra.Command{ - Use: "create [environment_name]", - Short: "create a new environment.", + Use: "create [workspace_name]", + Short: "create a new workspace.", Args: xcobra.ExactArgs(1), - Long: "Create a new Coder environment.", - Example: `# create a new environment using default resource amounts -coder envs create my-new-env --image ubuntu -coder envs create my-new-powerful-env --cpu 12 --disk 100 --memory 16 --image ubuntu`, + Long: "Create a new Coder workspace.", + Example: `# create a new workspace using default resource amounts +coder workspaces create my-new-workspace --image ubuntu +coder workspaces create my-new-powerful-workspace --cpu 12 --disk 100 --memory 16 --image ubuntu`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() if img == "" { @@ -225,7 +234,7 @@ coder envs create my-new-powerful-env --cpu 12 --disk 100 --memory 16 --image ub } // ExactArgs(1) ensures our name value can't panic on an out of bounds. - createReq := &coder.CreateEnvironmentRequest{ + createReq := &coder.CreateWorkspaceRequest{ Name: args[0], ImageID: importedImg.ID, OrgID: importedImg.OrganizationID, @@ -252,66 +261,66 @@ coder envs create my-new-powerful-env --cpu 12 --disk 100 --memory 16 --image ub createReq.DiskGB = importedImg.DefaultDiskGB } - env, err := client.CreateEnvironment(ctx, *createReq) + workspace, err := client.CreateWorkspace(ctx, *createReq) if err != nil { - return xerrors.Errorf("create environment: %w", err) + return xerrors.Errorf("create workspace: %w", err) } if follow { - clog.LogSuccess("creating environment...") - if err := trailBuildLogs(ctx, client, env.ID); err != nil { + clog.LogSuccess("creating workspace...") + if err := trailBuildLogs(ctx, client, workspace.ID); err != nil { return err } return nil } - clog.LogSuccess("creating environment...", + clog.LogSuccess("creating workspace...", clog.BlankLine, - clog.Tipf(`run "coder envs watch-build %s" to trail the build logs`, env.Name), + clog.Tipf(`run "coder workspaces watch-build %s" to trail the build logs`, workspace.Name), ) return nil }, } - cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the environment should be created under.") - cmd.Flags().StringVarP(&tag, "tag", "t", defaultImgTag, "tag of the image the environment will be based off of.") - cmd.Flags().Float32VarP(&cpu, "cpu", "c", 0, "number of cpu cores the environment should be provisioned with.") - cmd.Flags().Float32VarP(&memory, "memory", "m", 0, "GB of RAM an environment should be provisioned with.") - cmd.Flags().IntVarP(&disk, "disk", "d", 0, "GB of disk storage an environment should be provisioned with.") - cmd.Flags().IntVarP(&gpus, "gpus", "g", 0, "number GPUs an environment should be provisioned with.") - cmd.Flags().StringVarP(&img, "image", "i", "", "name of the image to base the environment off of.") - cmd.Flags().StringVar(&providerName, "provider", "", "name of Workspace Provider with which to create the environment") + cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the workspace should be created under.") + cmd.Flags().StringVarP(&tag, "tag", "t", defaultImgTag, "tag of the image the workspace will be based off of.") + cmd.Flags().Float32VarP(&cpu, "cpu", "c", 0, "number of cpu cores the workspace should be provisioned with.") + cmd.Flags().Float32VarP(&memory, "memory", "m", 0, "GB of RAM a workspace should be provisioned with.") + cmd.Flags().IntVarP(&disk, "disk", "d", 0, "GB of disk storage a workspace should be provisioned with.") + cmd.Flags().IntVarP(&gpus, "gpus", "g", 0, "number GPUs a workspace should be provisioned with.") + cmd.Flags().StringVarP(&img, "image", "i", "", "name of the image to base the workspace off of.") + cmd.Flags().StringVar(&providerName, "provider", "", "name of Workspace Provider with which to create the workspace") cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild") - cmd.Flags().BoolVar(&useCVM, "container-based-vm", false, "deploy the environment as a Container-based VM") - cmd.Flags().BoolVar(&enableAutostart, "enable-autostart", false, "automatically start this environment at your preferred time.") + cmd.Flags().BoolVar(&useCVM, "container-based-vm", false, "deploy the workspace as a Container-based VM") + cmd.Flags().BoolVar(&enableAutostart, "enable-autostart", false, "automatically start this workspace at your preferred time.") _ = cmd.MarkFlagRequired("image") return cmd } -func createEnvFromConfigCmd() *cobra.Command { +func createWorkspaceFromConfigCmd() *cobra.Command { var ( - ref string - repo string - follow bool - filepath string - org string - providerName string - envName string + ref string + repo string + follow bool + filepath string + org string + providerName string + workspaceName string ) cmd := &cobra.Command{ Use: "create-from-config", - Short: "create a new environment from a template", - Long: "Create a new Coder environment using a Workspaces As Code template.", - Example: `# create a new environment from git repository -coder envs create-from-config --name="dev-env" --repo-url https://github.com/cdr/m --ref my-branch -coder envs create-from-config --name="dev-env" -f coder.yaml`, + Short: "create a new workspace from a template", + Long: "Create a new Coder workspace using a Workspaces As Code template.", + Example: `# create a new workspace from git repository +coder workspaces create-from-config --name="dev-workspace" --repo-url https://github.com/cdr/m --ref my-branch +coder workspaces create-from-config --name="dev-workspace" -f coder.yaml`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - if envName == "" { - return clog.Error("Must provide a environment name.", + if workspaceName == "" { + return clog.Error("Must provide a workspace name.", clog.BlankLine, - clog.Tipf("Use --name= to name your environment"), + clog.Tipf("Use --name= to name your workspace"), ) } @@ -378,43 +387,43 @@ coder envs create-from-config --name="dev-env" -f coder.yaml`, return xerrors.Errorf("default workspace provider: %w", err) } - env, err := client.CreateEnvironment(ctx, coder.CreateEnvironmentRequest{ + workspace, err := client.CreateWorkspace(ctx, coder.CreateWorkspaceRequest{ OrgID: userOrg.ID, TemplateID: version.TemplateID, ResourcePoolID: provider.ID, Namespace: provider.DefaultNamespace, - Name: envName, + Name: workspaceName, }) if err != nil { return handleAPIError(err) } if follow { - clog.LogSuccess("creating environment...") - if err := trailBuildLogs(ctx, client, env.ID); err != nil { + clog.LogSuccess("creating workspace...") + if err := trailBuildLogs(ctx, client, workspace.ID); err != nil { return err } return nil } - clog.LogSuccess("creating environment...", + clog.LogSuccess("creating workspace...", clog.BlankLine, - clog.Tipf(`run "coder envs watch-build %s" to trail the build logs`, env.Name), + clog.Tipf(`run "coder workspaces watch-build %s" to trail the build logs`, workspace.Name), ) return nil }, } - cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the environment should be created under.") + cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the workspace should be created under.") cmd.Flags().StringVarP(&filepath, "filepath", "f", "", "path to local template file.") cmd.Flags().StringVarP(&ref, "ref", "", "master", "git reference to pull template from. May be a branch, tag, or commit hash.") cmd.Flags().StringVarP(&repo, "repo-url", "r", "", "URL of the git repository to pull the config from. Config file must live in '.coder/coder.yaml'.") cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild") - cmd.Flags().StringVar(&providerName, "provider", "", "name of Workspace Provider with which to create the environment") - cmd.Flags().StringVar(&envName, "name", "", "name of the environment to be created") + cmd.Flags().StringVar(&providerName, "provider", "", "name of Workspace Provider with which to create the workspace") + cmd.Flags().StringVar(&workspaceName, "name", "", "name of the workspace to be created") return cmd } -func editEnvCmd() *cobra.Command { +func editWorkspaceCmd() *cobra.Command { var ( org string img string @@ -430,12 +439,12 @@ func editEnvCmd() *cobra.Command { cmd := &cobra.Command{ Use: "edit", - Short: "edit an existing environment and initiate a rebuild.", + Short: "edit an existing workspace and initiate a rebuild.", Args: xcobra.ExactArgs(1), - Long: "Edit an existing environment and initate a rebuild.", - Example: `coder envs edit back-end-env --cpu 4 + Long: "Edit an existing workspace and initate a rebuild.", + Example: `coder workspaces edit back-end-workspace --cpu 4 -coder envs edit back-end-env --disk 20`, +coder workspaces edit back-end-workspace --disk 20`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() client, err := newClient(ctx, true) @@ -443,9 +452,9 @@ coder envs edit back-end-env --disk 20`, return err } - envName := args[0] + workspaceName := args[0] - env, err := findEnv(ctx, client, envName, user) + workspace, err := findWorkspace(ctx, client, workspaceName, user) if err != nil { return err } @@ -461,23 +470,23 @@ coder envs edit back-end-env --disk 20`, } req, err := buildUpdateReq(ctx, client, updateConf{ - cpu: cpu, - memGB: memory, - diskGB: disk, - gpus: gpus, - environment: env, - user: user, - image: img, - imageTag: tag, - orgName: org, + cpu: cpu, + memGB: memory, + diskGB: disk, + gpus: gpus, + workspace: workspace, + user: user, + image: img, + imageTag: tag, + orgName: org, }) if err != nil { return err } - if !force && env.LatestStat.ContainerStatus == coder.EnvironmentOn { + if !force && workspace.LatestStat.ContainerStatus == coder.WorkspaceOn { _, err = (&promptui.Prompt{ - Label: fmt.Sprintf("Rebuild environment %q? (will destroy any work outside of your home directory)", env.Name), + Label: fmt.Sprintf("Rebuild workspace %q? (will destroy any work outside of your home directory)", workspace.Name), IsConfirm: true, }).Run() if err != nil { @@ -488,47 +497,47 @@ coder envs edit back-end-env --disk 20`, } } - if err := client.EditEnvironment(ctx, env.ID, *req); err != nil { - return xerrors.Errorf("failed to apply changes to environment %q: %w", envName, err) + if err := client.EditWorkspace(ctx, workspace.ID, *req); err != nil { + return xerrors.Errorf("failed to apply changes to workspace %q: %w", workspaceName, err) } if follow { - clog.LogSuccess("applied changes to the environment, rebuilding...") - if err := trailBuildLogs(ctx, client, env.ID); err != nil { + clog.LogSuccess("applied changes to the workspace, rebuilding...") + if err := trailBuildLogs(ctx, client, workspace.ID); err != nil { return err } return nil } - clog.LogSuccess("applied changes to the environment, rebuilding...", + clog.LogSuccess("applied changes to the workspace, rebuilding...", clog.BlankLine, - clog.Tipf(`run "coder envs watch-build %s" to trail the build logs`, envName), + clog.Tipf(`run "coder workspaces watch-build %s" to trail the build logs`, workspaceName), ) return nil }, } - cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the environment should be created under.") - cmd.Flags().StringVarP(&img, "image", "i", "", "name of the image you want the environment to be based off of.") - cmd.Flags().StringVarP(&tag, "tag", "t", "latest", "image tag of the image you want to base the environment off of.") - cmd.Flags().Float32VarP(&cpu, "cpu", "c", 0, "The number of cpu cores the environment should be provisioned with.") - cmd.Flags().Float32VarP(&memory, "memory", "m", 0, "The amount of RAM an environment should be provisioned with.") - cmd.Flags().IntVarP(&disk, "disk", "d", 0, "The amount of disk storage an environment should be provisioned with.") - cmd.Flags().IntVarP(&gpus, "gpu", "g", 0, "The amount of disk storage to provision the environment with.") + cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the workspace should be created under.") + cmd.Flags().StringVarP(&img, "image", "i", "", "name of the image you want the workspace to be based off of.") + cmd.Flags().StringVarP(&tag, "tag", "t", "latest", "image tag of the image you want to base the workspace off of.") + cmd.Flags().Float32VarP(&cpu, "cpu", "c", 0, "The number of cpu cores the workspace should be provisioned with.") + cmd.Flags().Float32VarP(&memory, "memory", "m", 0, "The amount of RAM a workspace should be provisioned with.") + cmd.Flags().IntVarP(&disk, "disk", "d", 0, "The amount of disk storage a workspace should be provisioned with.") + cmd.Flags().IntVarP(&gpus, "gpu", "g", 0, "The amount of disk storage to provision the workspace with.") cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild") cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") cmd.Flags().BoolVar(&force, "force", false, "force rebuild without showing a confirmation prompt") return cmd } -func rmEnvsCmd() *cobra.Command { +func rmWorkspacesCmd() *cobra.Command { var ( force bool user string ) cmd := &cobra.Command{ - Use: "rm [...environment_names]", - Short: "remove Coder environments by name", + Use: "rm [...workspace_names]", + Short: "remove Coder workspaces by name", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -538,7 +547,7 @@ func rmEnvsCmd() *cobra.Command { } if !force { confirm := promptui.Prompt{ - Label: fmt.Sprintf("Delete environments %q? (all data will be lost)", args), + Label: fmt.Sprintf("Delete workspaces %q? (all data will be lost)", args), IsConfirm: true, } if _, err := confirm.Run(); err != nil { @@ -550,52 +559,52 @@ func rmEnvsCmd() *cobra.Command { } egroup := clog.LoggedErrGroup() - for _, envName := range args { - envName := envName + for _, workspaceName := range args { + workspaceName := workspaceName egroup.Go(func() error { - env, err := findEnv(ctx, client, envName, user) + workspace, err := findWorkspace(ctx, client, workspaceName, user) if err != nil { return err } - if err = client.DeleteEnvironment(ctx, env.ID); err != nil { + if err = client.DeleteWorkspace(ctx, workspace.ID); err != nil { return clog.Error( - fmt.Sprintf(`failed to delete environment "%s"`, env.Name), + fmt.Sprintf(`failed to delete workspace "%s"`, workspace.Name), clog.Causef(err.Error()), ) } - clog.LogSuccess(fmt.Sprintf("deleted environment %q", env.Name)) + clog.LogSuccess(fmt.Sprintf("deleted workspace %q", workspace.Name)) return nil }) } return egroup.Wait() }, } - cmd.Flags().BoolVarP(&force, "force", "f", false, "force remove the specified environments without prompting first") + cmd.Flags().BoolVarP(&force, "force", "f", false, "force remove the specified workspaces without prompting first") cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") return cmd } type updateConf struct { - cpu float32 - memGB float32 - diskGB int - gpus int - environment *coder.Environment - user string - image string - imageTag string - orgName string + cpu float32 + memGB float32 + diskGB int + gpus int + workspace *coder.Workspace + user string + image string + imageTag string + orgName string } -func buildUpdateReq(ctx context.Context, client coder.Client, conf updateConf) (*coder.UpdateEnvironmentReq, error) { +func buildUpdateReq(ctx context.Context, client coder.Client, conf updateConf) (*coder.UpdateWorkspaceReq, error) { var ( - updateReq coder.UpdateEnvironmentReq + updateReq coder.UpdateWorkspaceReq defaultCPUCores float32 defaultMemGB float32 defaultDiskGB int ) - // If this is not empty it means the user is requesting to change the environment image. + // If this is not empty it means the user is requesting to change the workspace image. if conf.image != "" { importedImg, err := findImg(ctx, client, findImgConf{ email: conf.user, @@ -607,12 +616,12 @@ func buildUpdateReq(ctx context.Context, client coder.Client, conf updateConf) ( } // If the user passes an image arg of the image that - // the environment is already using, it was most likely a mistake. + // the workspace is already using, it was most likely a mistake. if conf.image != importedImg.Repository { - return nil, xerrors.Errorf("environment is already using image %q", conf.image) + return nil, xerrors.Errorf("workspace is already using image %q", conf.image) } - // Since the environment image is being changed, + // Since the workspace image is being changed, // the resource amount defaults should be changed to // reflect that of the default resource amounts of the new image. defaultCPUCores = importedImg.DefaultCPUCores @@ -620,17 +629,17 @@ func buildUpdateReq(ctx context.Context, client coder.Client, conf updateConf) ( defaultDiskGB = importedImg.DefaultDiskGB updateReq.ImageID = &importedImg.ID } else { - // if the environment image is not being changed, the default + // if the workspace image is not being changed, the default // resource amounts should reflect the default resource amounts - // of the image the environment is already using. - defaultCPUCores = conf.environment.CPUCores - defaultMemGB = conf.environment.MemoryGB - defaultDiskGB = conf.environment.DiskGB - updateReq.ImageID = &conf.environment.ImageID + // of the image the workspace is already using. + defaultCPUCores = conf.workspace.CPUCores + defaultMemGB = conf.workspace.MemoryGB + defaultDiskGB = conf.workspace.DiskGB + updateReq.ImageID = &conf.workspace.ImageID } // The following logic checks to see if the user specified - // any resource amounts for the environment that need to be changed. + // any resource amounts for the workspace that need to be changed. // If they did not, then we will get the zero value back // and should set the resource amount to the default. @@ -652,14 +661,14 @@ func buildUpdateReq(ctx context.Context, client coder.Client, conf updateConf) ( updateReq.DiskGB = &conf.diskGB } - // Environment disks can not be shrink so we have to overwrite this + // Workspace disks can not be shrink so we have to overwrite this // if the user accidentally requests it or if the default diskGB value for a - // newly requested image is smaller than the current amount the environment is using. - if *updateReq.DiskGB < conf.environment.DiskGB { + // newly requested image is smaller than the current amount the workspace is using. + if *updateReq.DiskGB < conf.workspace.DiskGB { clog.LogWarn("disk can not be shrunk", - fmt.Sprintf("keeping environment disk at %d GB", conf.environment.DiskGB), + fmt.Sprintf("keeping workspace disk at %d GB", conf.workspace.DiskGB), ) - updateReq.DiskGB = &conf.environment.DiskGB + updateReq.DiskGB = &conf.workspace.DiskGB } if conf.gpus != 0 { diff --git a/internal/cmd/envs_test.go b/internal/cmd/workspaces_test.go similarity index 61% rename from internal/cmd/envs_test.go rename to internal/cmd/workspaces_test.go index b28ea6c7..a8c1d6cd 100644 --- a/internal/cmd/envs_test.go +++ b/internal/cmd/workspaces_test.go @@ -17,19 +17,19 @@ import ( "cdr.dev/coder-cli/coder-sdk" ) -func Test_envs_ls(t *testing.T) { +func Test_workspaces_ls(t *testing.T) { skipIfNoAuth(t) - res := execute(t, nil, "envs", "ls") + res := execute(t, nil, "workspaces", "ls") res.success(t) - res = execute(t, nil, "envs", "ls", "--output=json") + res = execute(t, nil, "workspaces", "ls", "--output=json") res.success(t) - var envs []coder.Environment - res.stdoutUnmarshals(t, &envs) + var workspaces []coder.Workspace + res.stdoutUnmarshals(t, &workspaces) } -func Test_envs_ls_by_provider(t *testing.T) { +func Test_workspaces_ls_by_provider(t *testing.T) { skipIfNoAuth(t) for _, test := range []struct { name string @@ -38,15 +38,15 @@ func Test_envs_ls_by_provider(t *testing.T) { }{ { name: "simple list", - command: []string{"envs", "ls", "--provider", "built-in"}, + command: []string{"workspaces", "ls", "--provider", "built-in"}, assert: func(r result) { r.success(t) }, }, { name: "list as json", - command: []string{"envs", "ls", "--provider", "built-in", "--output", "json"}, + command: []string{"workspaces", "ls", "--provider", "built-in", "--output", "json"}, assert: func(r result) { - var envs []coder.Environment - r.stdoutUnmarshals(t, &envs) + var workspaces []coder.Workspace + r.stdoutUnmarshals(t, &workspaces) }, }, } { @@ -57,27 +57,27 @@ func Test_envs_ls_by_provider(t *testing.T) { } } -func Test_env_create(t *testing.T) { +func Test_workspace_create(t *testing.T) { skipIfNoAuth(t) ctx := context.Background() // Minimum args not received. - res := execute(t, nil, "envs", "create") + res := execute(t, nil, "workspaces", "create") res.error(t) res.stderrContains(t, "accepts 1 arg(s), received 0") // Successfully output help. - res = execute(t, nil, "envs", "create", "--help") + res = execute(t, nil, "workspaces", "create", "--help") res.success(t) - res.stdoutContains(t, "Create a new Coder environment.") + res.stdoutContains(t, "Create a new Coder workspace.") // Image unset - res = execute(t, nil, "envs", "create", "test-env") + res = execute(t, nil, "workspaces", "create", "test-workspace") res.error(t) res.stderrContains(t, "fatal: required flag(s) \"image\" not set") // Image not imported - res = execute(t, nil, "envs", "create", "test-env", "--image=doestexist") + res = execute(t, nil, "workspaces", "create", "test-workspace", "--image=doestexist") res.error(t) res.stderrContains(t, "fatal: image not found - did you forget to import this image?") @@ -86,49 +86,49 @@ func Test_env_create(t *testing.T) { name := randString(10) cpu := 2.3 - // attempt to remove the environment on cleanup - t.Cleanup(func() { _ = execute(t, nil, "envs", "rm", name, "--force") }) + // attempt to remove the workspace on cleanup + t.Cleanup(func() { _ = execute(t, nil, "workspaces", "rm", name, "--force") }) - res = execute(t, nil, "envs", "create", name, "--image=ubuntu", fmt.Sprintf("--cpu=%f", cpu)) + res = execute(t, nil, "workspaces", "create", name, "--image=ubuntu", fmt.Sprintf("--cpu=%f", cpu)) res.success(t) - res = execute(t, nil, "envs", "ls") + res = execute(t, nil, "workspaces", "ls") res.success(t) res.stdoutContains(t, name) - var envs []coder.Environment - res = execute(t, nil, "envs", "ls", "--output=json") + var workspaces []coder.Workspace + res = execute(t, nil, "workspaces", "ls", "--output=json") res.success(t) - res.stdoutUnmarshals(t, &envs) - env := assertEnv(t, name, envs) - assert.Equal(t, "env cpu", cpu, float64(env.CPUCores), floatComparer) + res.stdoutUnmarshals(t, &workspaces) + workspace := assertWorkspace(t, name, workspaces) + assert.Equal(t, "workspace cpu", cpu, float64(workspace.CPUCores), floatComparer) - res = execute(t, nil, "envs", "watch-build", name) + res = execute(t, nil, "workspaces", "watch-build", name) res.success(t) - // edit the CPU of the environment + // edit the CPU of the workspace cpu = 2.1 - res = execute(t, nil, "envs", "edit", name, fmt.Sprintf("--cpu=%f", cpu), "--follow", "--force") + res = execute(t, nil, "workspaces", "edit", name, fmt.Sprintf("--cpu=%f", cpu), "--follow", "--force") res.success(t) // assert that the CPU actually did change after edit - res = execute(t, nil, "envs", "ls", "--output=json") + res = execute(t, nil, "workspaces", "ls", "--output=json") res.success(t) - res.stdoutUnmarshals(t, &envs) - env = assertEnv(t, name, envs) - assert.Equal(t, "env cpu", cpu, float64(env.CPUCores), floatComparer) + res.stdoutUnmarshals(t, &workspaces) + workspace = assertWorkspace(t, name, workspaces) + assert.Equal(t, "workspace cpu", cpu, float64(workspace.CPUCores), floatComparer) - res = execute(t, nil, "envs", "rm", name, "--force") + res = execute(t, nil, "workspaces", "rm", name, "--force") res.success(t) } -func assertEnv(t *testing.T, name string, envs []coder.Environment) *coder.Environment { - for _, e := range envs { +func assertWorkspace(t *testing.T, name string, workspaces []coder.Workspace) *coder.Workspace { + for _, e := range workspaces { if name == e.Name { return &e } } - slogtest.Fatal(t, "env not found", slog.F("name", name), slog.F("envs", envs)) + slogtest.Fatal(t, "workspace not found", slog.F("name", name), slog.F("workspaces", workspaces)) return nil } diff --git a/internal/coderutil/env.go b/internal/coderutil/workspace.go similarity index 54% rename from internal/coderutil/env.go rename to internal/coderutil/workspace.go index 9485347f..b81ad964 100644 --- a/internal/coderutil/env.go +++ b/internal/coderutil/workspace.go @@ -13,34 +13,34 @@ import ( "cdr.dev/coder-cli/pkg/clog" ) -// DialEnvWsep dials the executor endpoint using the https://github.com/cdr/wsep message protocol. +// DialWorkspaceWsep dials the executor endpoint using the https://github.com/cdr/wsep message protocol. // The proper workspace provider envproxy access URL is used. -func DialEnvWsep(ctx context.Context, client coder.Client, env *coder.Environment) (*websocket.Conn, error) { - workspaceProvider, err := client.WorkspaceProviderByID(ctx, env.ResourcePoolID) +func DialWorkspaceWsep(ctx context.Context, client coder.Client, workspace *coder.Workspace) (*websocket.Conn, error) { + workspaceProvider, err := client.WorkspaceProviderByID(ctx, workspace.ResourcePoolID) if err != nil { - return nil, xerrors.Errorf("get env workspace provider: %w", err) + return nil, xerrors.Errorf("get workspace workspace provider: %w", err) } accessURL, err := url.Parse(workspaceProvider.EnvproxyAccessURL) if err != nil { return nil, xerrors.Errorf("invalid workspace provider envproxy access url: %w", err) } - conn, err := client.DialWsep(ctx, accessURL, env.ID) + conn, err := client.DialWsep(ctx, accessURL, workspace.ID) if err != nil { return nil, xerrors.Errorf("dial websocket: %w", err) } return conn, nil } -// EnvWithWorkspaceProvider composes an Environment entity with its associated WorkspaceProvider. -type EnvWithWorkspaceProvider struct { - Env coder.Environment +// WorkspaceWithWorkspaceProvider composes an Workspace entity with its associated WorkspaceProvider. +type WorkspaceWithWorkspaceProvider struct { + Workspace coder.Workspace WorkspaceProvider coder.KubernetesProvider } -// EnvsWithProvider performs the composition of each Environment with its associated WorkspaceProvider. -func EnvsWithProvider(ctx context.Context, client coder.Client, envs []coder.Environment) ([]EnvWithWorkspaceProvider, error) { - pooledEnvs := make([]EnvWithWorkspaceProvider, 0, len(envs)) +// WorkspacesWithProvider performs the composition of each Workspace with its associated WorkspaceProvider. +func WorkspacesWithProvider(ctx context.Context, client coder.Client, workspaces []coder.Workspace) ([]WorkspaceWithWorkspaceProvider, error) { + pooledWorkspaces := make([]WorkspaceWithWorkspaceProvider, 0, len(workspaces)) providers, err := client.WorkspaceProviders(ctx) if err != nil { return nil, err @@ -49,20 +49,20 @@ func EnvsWithProvider(ctx context.Context, client coder.Client, envs []coder.Env for _, p := range providers.Kubernetes { providerMap[p.ID] = p } - for _, e := range envs { - envProvider, ok := providerMap[e.ResourcePoolID] + for _, e := range workspaces { + workspaceProvider, ok := providerMap[e.ResourcePoolID] if !ok { - return nil, xerrors.Errorf("fetch env workspace provider: %w", coder.ErrNotFound) + return nil, xerrors.Errorf("fetch workspace workspace provider: %w", coder.ErrNotFound) } - pooledEnvs = append(pooledEnvs, EnvWithWorkspaceProvider{ - Env: e, - WorkspaceProvider: envProvider, + pooledWorkspaces = append(pooledWorkspaces, WorkspaceWithWorkspaceProvider{ + Workspace: e, + WorkspaceProvider: workspaceProvider, }) } - return pooledEnvs, nil + return pooledWorkspaces, nil } -// DefaultWorkspaceProvider returns the default provider with which to create environments. +// DefaultWorkspaceProvider returns the default provider with which to create workspaces. func DefaultWorkspaceProvider(ctx context.Context, c coder.Client) (*coder.KubernetesProvider, error) { provider, err := c.WorkspaceProviders(ctx) if err != nil { @@ -76,9 +76,9 @@ func DefaultWorkspaceProvider(ctx context.Context, c coder.Client) (*coder.Kuber return nil, coder.ErrNotFound } -// EnvTable defines an Environment-like structure with associated entities composed in a human +// WorkspaceTable defines an Workspace-like structure with associated entities composed in a human // readable form. -type EnvTable struct { +type WorkspaceTable struct { Name string `table:"Name"` Image string `table:"Image"` CPU float32 `table:"vCPU"` @@ -89,14 +89,14 @@ type EnvTable struct { CVM bool `table:"CVM"` } -// EnvsHumanTable performs the composition of each Environment with its associated ProviderName and ImageRepo. -func EnvsHumanTable(ctx context.Context, client coder.Client, envs []coder.Environment) ([]EnvTable, error) { - imageMap, err := MakeImageMap(ctx, client, envs) +// WorkspacesHumanTable performs the composition of each Workspace with its associated ProviderName and ImageRepo. +func WorkspacesHumanTable(ctx context.Context, client coder.Client, workspaces []coder.Workspace) ([]WorkspaceTable, error) { + imageMap, err := MakeImageMap(ctx, client, workspaces) if err != nil { return nil, err } - pooledEnvs := make([]EnvTable, 0, len(envs)) + pooledWorkspaces := make([]WorkspaceTable, 0, len(workspaces)) providers, err := client.WorkspaceProviders(ctx) if err != nil { return nil, err @@ -105,33 +105,33 @@ func EnvsHumanTable(ctx context.Context, client coder.Client, envs []coder.Envir for _, p := range providers.Kubernetes { providerMap[p.ID] = p } - for _, e := range envs { - envProvider, ok := providerMap[e.ResourcePoolID] + for _, e := range workspaces { + workspaceProvider, ok := providerMap[e.ResourcePoolID] if !ok { - return nil, xerrors.Errorf("fetch env workspace provider: %w", coder.ErrNotFound) + return nil, xerrors.Errorf("fetch workspace workspace provider: %w", coder.ErrNotFound) } - pooledEnvs = append(pooledEnvs, EnvTable{ + pooledWorkspaces = append(pooledWorkspaces, WorkspaceTable{ Name: e.Name, Image: fmt.Sprintf("%s:%s", imageMap[e.ImageID].Repository, e.ImageTag), CPU: e.CPUCores, MemoryGB: e.MemoryGB, DiskGB: e.DiskGB, Status: string(e.LatestStat.ContainerStatus), - Provider: envProvider.Name, + Provider: workspaceProvider.Name, CVM: e.UseContainerVM, }) } - return pooledEnvs, nil + return pooledWorkspaces, nil } -// MakeImageMap fetches all image entities specified in the slice of environments, then places them into an ID map. -func MakeImageMap(ctx context.Context, client coder.Client, envs []coder.Environment) (map[string]*coder.Image, error) { +// MakeImageMap fetches all image entities specified in the slice of workspaces, then places them into an ID map. +func MakeImageMap(ctx context.Context, client coder.Client, workspaces []coder.Workspace) (map[string]*coder.Image, error) { var ( mu sync.Mutex egroup = clog.LoggedErrGroup() ) imageMap := make(map[string]*coder.Image) - for _, e := range envs { + for _, e := range workspaces { // put all the image IDs into a map to remove duplicates imageMap[e.ImageID] = nil } diff --git a/internal/sync/singlefile.go b/internal/sync/singlefile.go index 0e15e354..adf8c290 100644 --- a/internal/sync/singlefile.go +++ b/internal/sync/singlefile.go @@ -17,9 +17,9 @@ import ( "cdr.dev/coder-cli/internal/coderutil" ) -// SingleFile copies the given file into the remote dir or remote path of the given coder.Environment. -func SingleFile(ctx context.Context, local, remoteDir string, env *coder.Environment, client coder.Client) error { - conn, err := coderutil.DialEnvWsep(ctx, client, env) +// SingleFile copies the given file into the remote dir or remote path of the given coder.Workspace. +func SingleFile(ctx context.Context, local, remoteDir string, workspace *coder.Workspace, client coder.Client) error { + conn, err := coderutil.DialWorkspaceWsep(ctx, client, workspace) if err != nil { return xerrors.Errorf("dial remote execer: %w", err) } diff --git a/internal/sync/sync.go b/internal/sync/sync.go index 1d356f0f..dd90cf7a 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -1,4 +1,4 @@ -// Package sync contains logic for establishing a file sync between a local machine and a Coder environment. +// Package sync contains logic for establishing a file sync between a local machine and a Coder workspace. package sync import ( @@ -41,7 +41,7 @@ type Sync struct { // DisableMetrics disables activity metric pushing. DisableMetrics bool - Env coder.Environment + Workspace coder.Workspace Client coder.Client OutW io.Writer ErrW io.Writer @@ -61,7 +61,7 @@ func (s Sync) syncPaths(delete bool, local, remote string) error { args := []string{"-zz", "-a", "--delete", - "-e", self + " sh", local, s.Env.Name + ":" + remote, + "-e", self + " sh", local, s.Workspace.Name + ":" + remote, } if delete { args = append([]string{"--delete"}, args...) @@ -95,7 +95,7 @@ func (s Sync) syncPaths(delete bool, local, remote string) error { } func (s Sync) remoteCmd(ctx context.Context, prog string, args ...string) error { - conn, err := coderutil.DialEnvWsep(ctx, s.Client, &s.Env) + conn, err := coderutil.DialWorkspaceWsep(ctx, s.Client, &s.Workspace) if err != nil { return xerrors.Errorf("dial executor: %w", err) } @@ -276,9 +276,9 @@ func (s Sync) Version() (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - conn, err := coderutil.DialEnvWsep(ctx, s.Client, &s.Env) + conn, err := coderutil.DialWorkspaceWsep(ctx, s.Client, &s.Workspace) if err != nil { - return "", xerrors.Errorf("dial env executor: %w", err) + return "", xerrors.Errorf("dial workspace executor: %w", err) } defer func() { _ = conn.Close(websocket.CloseNormalClosure, "") }() // Best effort. @@ -327,7 +327,7 @@ func (s Sync) Run() error { return xerrors.Errorf("create remote directory: %w", err) } - ap := activity.NewPusher(s.Client, s.Env.ID, activityName) + ap := activity.NewPusher(s.Client, s.Workspace.ID, activityName) ap.Push(ctx) setConsoleTitle("⏳ syncing project", s.IsInteractiveOutput) diff --git a/pkg/clog/errgroup.go b/pkg/clog/errgroup.go index e3a16fd4..a96d6449 100644 --- a/pkg/clog/errgroup.go +++ b/pkg/clog/errgroup.go @@ -10,7 +10,7 @@ import ( // ErrGroup wraps the /x/sync/errgroup.(Group) and adds clog logging and rich error propagation. // -// Take for example, a case in which we are concurrently stopping a slice of environments. +// Take for example, a case in which we are concurrently stopping a slice of workspaces. // In this case, we want to log errors as they happen, not pass them through the callstack as errors. // When the operations complete, we want to log how many, if any, failed. The caller is still expected // to handle success and info logging. From 188450b3cb67970ef586213c3051d63efc1e0ada Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 17 May 2021 17:41:58 -0500 Subject: [PATCH 035/128] fix: Drop port when creating TURN endpoint (#352) --- wsnet/conn.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wsnet/conn.go b/wsnet/conn.go index 35d3aff2..7e18723b 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -19,7 +19,8 @@ func TURNEndpoint(baseURL *url.URL) string { if baseURL.Scheme == httpScheme { turnScheme = "turn" } - return fmt.Sprintf("%s:%s:5349?transport=tcp", turnScheme, baseURL.Host) + + return fmt.Sprintf("%s:%s:5349?transport=tcp", turnScheme, baseURL.Hostname()) } // ListenEndpoint returns the Coder endpoint to listen for workspace connections. From 74d02942c883da830b35324be690a04e81ec895d Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 18 May 2021 13:58:57 -0500 Subject: [PATCH 036/128] feat: Add edit-from-config command to switch templates (#332) - Add edit-from-config command to switch templates. Only works for local templates --- coder-sdk/workspace.go | 13 +- docs/coder_workspaces.md | 1 + docs/coder_workspaces_create-from-config.md | 4 +- docs/coder_workspaces_edit-from-config.md | 38 +++ internal/cmd/workspaces.go | 260 ++++++++++++-------- 5 files changed, 211 insertions(+), 105 deletions(-) create mode 100644 docs/coder_workspaces_edit-from-config.md diff --git a/coder-sdk/workspace.go b/coder-sdk/workspace.go index fca61dcd..e48d4bc7 100644 --- a/coder-sdk/workspace.go +++ b/coder-sdk/workspace.go @@ -205,12 +205,13 @@ func (c *DefaultClient) StopWorkspace(ctx context.Context, workspaceID string) e // UpdateWorkspaceReq defines the update operation, only setting // nil-fields. type UpdateWorkspaceReq struct { - ImageID *string `json:"image_id"` - ImageTag *string `json:"image_tag"` - CPUCores *float32 `json:"cpu_cores"` - MemoryGB *float32 `json:"memory_gb"` - DiskGB *int `json:"disk_gb"` - GPUs *int `json:"gpus"` + ImageID *string `json:"image_id"` + ImageTag *string `json:"image_tag"` + CPUCores *float32 `json:"cpu_cores"` + MemoryGB *float32 `json:"memory_gb"` + DiskGB *int `json:"disk_gb"` + GPUs *int `json:"gpus"` + TemplateID *string `json:"template_id"` } // RebuildWorkspace requests that the given workspaceID is rebuilt with no changes to its specification. diff --git a/docs/coder_workspaces.md b/docs/coder_workspaces.md index bb29bcf5..4402d92c 100644 --- a/docs/coder_workspaces.md +++ b/docs/coder_workspaces.md @@ -24,6 +24,7 @@ Perform operations on the Coder workspaces owned by the active user. * [coder workspaces create](coder_workspaces_create.md) - create a new workspace. * [coder workspaces create-from-config](coder_workspaces_create-from-config.md) - create a new workspace from a template * [coder workspaces edit](coder_workspaces_edit.md) - edit an existing workspace and initiate a rebuild. +* [coder workspaces edit-from-config](coder_workspaces_edit-from-config.md) - change the template a workspace is tracking * [coder workspaces ls](coder_workspaces_ls.md) - list all workspaces owned by the active user * [coder workspaces rebuild](coder_workspaces_rebuild.md) - rebuild a Coder workspace * [coder workspaces rm](coder_workspaces_rm.md) - remove Coder workspaces by name diff --git a/docs/coder_workspaces_create-from-config.md b/docs/coder_workspaces_create-from-config.md index 3ad6c2ae..221175b7 100644 --- a/docs/coder_workspaces_create-from-config.md +++ b/docs/coder_workspaces_create-from-config.md @@ -14,8 +14,8 @@ coder workspaces create-from-config [flags] ``` # create a new workspace from git repository -coder workspaces create-from-config --name="dev-workspace" --repo-url https://github.com/cdr/m --ref my-branch -coder workspaces create-from-config --name="dev-workspace" -f coder.yaml +coder envs create-from-config --name="dev-env" --repo-url https://github.com/cdr/m --ref my-branch +coder envs create-from-config --name="dev-env" -f coder.yaml ``` ### Options diff --git a/docs/coder_workspaces_edit-from-config.md b/docs/coder_workspaces_edit-from-config.md new file mode 100644 index 00000000..085e1470 --- /dev/null +++ b/docs/coder_workspaces_edit-from-config.md @@ -0,0 +1,38 @@ +## coder workspaces edit-from-config + +change the template a workspace is tracking + +### Synopsis + +Edit an existing Coder workspace using a Workspaces As Code template. + +``` +coder workspaces edit-from-config [flags] +``` + +### Examples + +``` +# edit a new workspace from git repository +coder envs edit-from-config dev-env --repo-url https://github.com/cdr/m --ref my-branch +coder envs edit-from-config dev-env -f coder.yaml +``` + +### Options + +``` + -f, --filepath string path to local template file. + --follow follow buildlog after initiating rebuild + -h, --help help for edit-from-config +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces + diff --git a/internal/cmd/workspaces.go b/internal/cmd/workspaces.go index 2660dc3e..b8017baa 100644 --- a/internal/cmd/workspaces.go +++ b/internal/cmd/workspaces.go @@ -44,7 +44,8 @@ func workspacesCmd() *cobra.Command { watchBuildLogCommand(), rebuildWorkspaceCommand(), createWorkspaceCmd(), - createWorkspaceFromConfigCmd(), + workspaceFromConfigCmd(true), + workspaceFromConfigCmd(false), editWorkspaceCmd(), ) return cmd @@ -296,130 +297,195 @@ coder workspaces create my-new-powerful-workspace --cpu 12 --disk 100 --memory 1 return cmd } -func createWorkspaceFromConfigCmd() *cobra.Command { +// selectOrg finds the organization in the list or returns the default organization +// if the needle isn't found. +func selectOrg(needle string, haystack []coder.Organization) (*coder.Organization, error) { + var userOrg *coder.Organization + for i := range haystack { + // Look for org by name + if haystack[i].Name == needle { + userOrg = &haystack[i] + break + } + // Or use default if the provided is blank + if needle == "" && haystack[i].Default { + userOrg = &haystack[i] + break + } + } + + if userOrg == nil { + if needle != "" { + return nil, xerrors.Errorf("Unable to locate org '%s'", needle) + } + return nil, xerrors.Errorf("Unable to locate a default organization for the user") + } + return userOrg, nil +} + +// workspaceFromConfigCmd will return a create or an update workspace for a template'd workspace. +// The code for create/update is nearly identical. +// If `update` is true, the update command is returned. If false, the create command. +func workspaceFromConfigCmd(update bool) *cobra.Command { var ( - ref string - repo string - follow bool - filepath string - org string - providerName string - workspaceName string + ref string + repo string + follow bool + filepath string + org string + providerName string + envName string ) - cmd := &cobra.Command{ - Use: "create-from-config", - Short: "create a new workspace from a template", - Long: "Create a new Coder workspace using a Workspaces As Code template.", - Example: `# create a new workspace from git repository -coder workspaces create-from-config --name="dev-workspace" --repo-url https://github.com/cdr/m --ref my-branch -coder workspaces create-from-config --name="dev-workspace" -f coder.yaml`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() + run := func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() - if workspaceName == "" { - return clog.Error("Must provide a workspace name.", - clog.BlankLine, - clog.Tipf("Use --name= to name your workspace"), - ) - } + // Update requires the env name, and the name should be the first argument. + if update { + envName = args[0] + } else if envName == "" { + // Create takes the name as a flag, and it must be set + return clog.Error("Must provide a workspace name.", + clog.BlankLine, + clog.Tipf("Use --name= to name your workspace"), + ) + } - client, err := newClient(ctx, true) + client, err := newClient(ctx, true) + if err != nil { + return err + } + + orgs, err := getUserOrgs(ctx, client, coder.Me) + if err != nil { + return err + } + + multiOrgMember := len(orgs) > 1 + if multiOrgMember && org == "" { + return xerrors.New("org is required for multi-org members") + } + + // This is the env to be updated/created + var env *coder.Workspace + + // OrgID is the org where the template and env should be created. + // If we are updating an env, use the orgID from the workspace. + var orgID string + if update { + env, err = findWorkspace(ctx, client, envName, coder.Me) if err != nil { - return err + return handleAPIError(err) } - - orgs, err := getUserOrgs(ctx, client, coder.Me) + orgID = env.OrganizationID + } else { + var userOrg *coder.Organization + // Select org in list or use default + userOrg, err := selectOrg(org, orgs) if err != nil { return err } - multiOrgMember := len(orgs) > 1 - if multiOrgMember && org == "" { - return xerrors.New("org is required for multi-org members") - } - - var userOrg *coder.Organization - for i := range orgs { - // Look for org by name - if orgs[i].Name == org { - userOrg = &orgs[i] - break - } - // Or use default if the provided is blank - if org == "" && orgs[i].Default { - userOrg = &orgs[i] - break - } - } + orgID = userOrg.ID + } - if userOrg == nil { - if org != "" { - return xerrors.Errorf("Unable to locate org '%s'", org) - } - return xerrors.Errorf("Unable to locate a default organization for the user") - } + if filepath == "" && ref == "" && repo == "" { + return clog.Error("Must specify a configuration source", + "A template source is either sourced from a local file (-f) or from a git repository (--repo-url and --ref)", + ) + } - var rd io.Reader - if filepath != "" { - b, err := ioutil.ReadFile(filepath) - if err != nil { - return xerrors.Errorf("read local file: %w", err) - } - rd = bytes.NewReader(b) + var rd io.Reader + if filepath != "" { + b, err := ioutil.ReadFile(filepath) + if err != nil { + return xerrors.Errorf("read local file: %w", err) } + rd = bytes.NewReader(b) + } - req := coder.ParseTemplateRequest{ - RepoURL: repo, - Ref: ref, - Local: rd, - OrgID: userOrg.ID, - Filepath: ".coder/coder.yaml", - } + req := coder.ParseTemplateRequest{ + RepoURL: repo, + Ref: ref, + Local: rd, + OrgID: orgID, + Filepath: ".coder/coder.yaml", + } - version, err := client.ParseTemplate(ctx, req) - if err != nil { - return handleAPIError(err) - } + version, err := client.ParseTemplate(ctx, req) + if err != nil { + return handleAPIError(err) + } - provider, err := coderutil.DefaultWorkspaceProvider(ctx, client) - if err != nil { - return xerrors.Errorf("default workspace provider: %w", err) - } + provider, err := coderutil.DefaultWorkspaceProvider(ctx, client) + if err != nil { + return xerrors.Errorf("default workspace provider: %w", err) + } - workspace, err := client.CreateWorkspace(ctx, coder.CreateWorkspaceRequest{ - OrgID: userOrg.ID, + if update { + err = client.EditWorkspace(ctx, env.ID, coder.UpdateWorkspaceReq{ + TemplateID: &version.TemplateID, + }) + } else { + env, err = client.CreateWorkspace(ctx, coder.CreateWorkspaceRequest{ + OrgID: orgID, TemplateID: version.TemplateID, ResourcePoolID: provider.ID, Namespace: provider.DefaultNamespace, - Name: workspaceName, + Name: envName, }) - if err != nil { - return handleAPIError(err) - } + } + if err != nil { + return handleAPIError(err) + } - if follow { - clog.LogSuccess("creating workspace...") - if err := trailBuildLogs(ctx, client, workspace.ID); err != nil { - return err - } - return nil + if follow { + clog.LogSuccess("creating workspace...") + if err := trailBuildLogs(ctx, client, env.ID); err != nil { + return err } - - clog.LogSuccess("creating workspace...", - clog.BlankLine, - clog.Tipf(`run "coder workspaces watch-build %s" to trail the build logs`, workspace.Name), - ) return nil - }, + } + + clog.LogSuccess("creating workspace...", + clog.BlankLine, + clog.Tipf(`run "coder envs watch-build %s" to trail the build logs`, env.Name), + ) + return nil } - cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the workspace should be created under.") + + var cmd *cobra.Command + if update { + cmd = &cobra.Command{ + Use: "edit-from-config", + Short: "change the template a workspace is tracking", + Long: "Edit an existing Coder workspace using a Workspaces As Code template.", + Args: cobra.ExactArgs(1), + Example: `# edit a new workspace from git repository +coder envs edit-from-config dev-env --repo-url https://github.com/cdr/m --ref my-branch +coder envs edit-from-config dev-env -f coder.yaml`, + RunE: run, + } + } else { + cmd = &cobra.Command{ + Use: "create-from-config", + Short: "create a new workspace from a template", + Long: "Create a new Coder workspace using a Workspaces As Code template.", + Example: `# create a new workspace from git repository +coder envs create-from-config --name="dev-env" --repo-url https://github.com/cdr/m --ref my-branch +coder envs create-from-config --name="dev-env" -f coder.yaml`, + RunE: run, + } + cmd.Flags().StringVar(&providerName, "provider", "", "name of Workspace Provider with which to create the workspace") + cmd.Flags().StringVar(&envName, "name", "", "name of the workspace to be created") + cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the workspace should be created under.") + // Ref and repo-url can only be used for create + cmd.Flags().StringVarP(&ref, "ref", "", "master", "git reference to pull template from. May be a branch, tag, or commit hash.") + cmd.Flags().StringVarP(&repo, "repo-url", "r", "", "URL of the git repository to pull the config from. Config file must live in '.coder/coder.yaml'.") + } + cmd.Flags().StringVarP(&filepath, "filepath", "f", "", "path to local template file.") - cmd.Flags().StringVarP(&ref, "ref", "", "master", "git reference to pull template from. May be a branch, tag, or commit hash.") - cmd.Flags().StringVarP(&repo, "repo-url", "r", "", "URL of the git repository to pull the config from. Config file must live in '.coder/coder.yaml'.") cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild") - cmd.Flags().StringVar(&providerName, "provider", "", "name of Workspace Provider with which to create the workspace") - cmd.Flags().StringVar(&workspaceName, "name", "", "name of the workspace to be created") return cmd } From bce838dbba3b6a538b11da9e50ed536a6dd816e1 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 20 May 2021 08:32:23 -0500 Subject: [PATCH 037/128] fix: Go version for release publish (#354) --- .github/workflows/release.yaml | 6 ++++++ wsnet/wsnet_test.go | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7ac0fdcf..19739a82 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -9,6 +9,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: '^1.16.3' - name: Build run: make -j build/linux build/windows - name: Upload linux @@ -36,6 +39,9 @@ jobs: with: p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} + - uses: actions/setup-go@v2 + with: + go-version: '^1.16.3' - name: Build Release Assets run: make build/macos env: diff --git a/wsnet/wsnet_test.go b/wsnet/wsnet_test.go index 10a3a5e2..6b7fa6f1 100644 --- a/wsnet/wsnet_test.go +++ b/wsnet/wsnet_test.go @@ -56,6 +56,11 @@ func createDumbBroker(t *testing.T) (connectAddr string, listenAddr string) { c, err := websocket.Accept(w, r, nil) if err != nil { t.Error(err) + return + } + if sess == nil { + t.Error("listen not called") + return } nc := websocket.NetConn(context.Background(), c, websocket.MessageBinary) mut.Lock() From bf7f71ab8d49e0799c2432fb8881f0e20752fbfa Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Thu, 20 May 2021 11:10:48 -0400 Subject: [PATCH 038/128] coder: remove some unnecessary SSH settings (#353) - Removed ServerAliveInterval and ServerAliveCountMax from config-ssh settings. These settings are opinionated about when idle sessions should disconnect and should be inherited from the user's global settings. 'coder config-ssh' should only write configuration options that are necessary for 'coder sh' and its variants to succeed. --- internal/cmd/configssh.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index 9417c514..7f79d6da 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -230,8 +230,6 @@ func makeSSHConfig(host, userName, workspaceName, privateKeyFilepath string, tun ConnectTimeout=0 IdentitiesOnly yes IdentityFile="%s" - ServerAliveInterval 60 - ServerAliveCountMax 3 `, workspaceName, workspaceName, workspaceName, privateKeyFilepath) } @@ -243,8 +241,6 @@ func makeSSHConfig(host, userName, workspaceName, privateKeyFilepath string, tun ConnectTimeout=0 IdentitiesOnly yes IdentityFile="%s" - ServerAliveInterval 60 - ServerAliveCountMax 3 `, workspaceName, host, userName, workspaceName, privateKeyFilepath) } From d6b2afd11e371ede74172eba7786743cc313160d Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 20 May 2021 15:40:11 -0500 Subject: [PATCH 039/128] fix: Test race when accessing session (#355) --- wsnet/wsnet_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wsnet/wsnet_test.go b/wsnet/wsnet_test.go index 6b7fa6f1..8452015d 100644 --- a/wsnet/wsnet_test.go +++ b/wsnet/wsnet_test.go @@ -58,13 +58,13 @@ func createDumbBroker(t *testing.T) (connectAddr string, listenAddr string) { t.Error(err) return } + nc := websocket.NetConn(context.Background(), c, websocket.MessageBinary) + mut.Lock() + defer mut.Unlock() if sess == nil { t.Error("listen not called") return } - nc := websocket.NetConn(context.Background(), c, websocket.MessageBinary) - mut.Lock() - defer mut.Unlock() oc, err := sess.Open() if err != nil { t.Error(err) From dcfd5015ce63be3dfbebd960b8d7a3d24f9d7571 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 21 May 2021 12:25:14 -0500 Subject: [PATCH 040/128] fix: Networking V2 tunnel on Windows (#356) --- cmd/coder/main.go | 24 +++++++++++++++--------- internal/cmd/configssh.go | 17 +++++++++++------ 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/cmd/coder/main.go b/cmd/coder/main.go index 51ce8614..93abeb3c 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -25,15 +25,21 @@ func main() { }() } - stdoutState, err := xterminal.MakeOutputRaw(os.Stdout.Fd()) - if err != nil { - clog.Log(clog.Fatal(fmt.Sprintf("set output to raw: %s", err))) - cancel() - os.Exit(1) - } - restoreTerminal := func() { - // Best effort. Would result in broken terminal on window but nothing we can do about it. - _ = xterminal.Restore(os.Stdout.Fd(), stdoutState) + restoreTerminal := func() {} + + // Janky, but SSH on windows sets the output to raw. + // If we set it ourselves, SSH fails because the FD isn't found. + if len(os.Args) >= 2 && os.Args[1] != "tunnel" { + state, err := xterminal.MakeOutputRaw(os.Stdout.Fd()) + if err != nil { + clog.Log(clog.Fatal(fmt.Sprintf("set output to raw: %s", err))) + cancel() + os.Exit(1) + } + restoreTerminal = func() { + // Best effort. Would result in broken terminal on window but nothing we can do about it. + _ = xterminal.Restore(os.Stdout.Fd(), state) + } } app := cmd.Make() diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index 7f79d6da..a482bbe7 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -132,7 +132,12 @@ func configSSH(configpath *string, remove *bool, next *bool) func(cmd *cobra.Com } } - newConfig := makeNewConfigs(user.Username, workspacesWithProviders, privateKeyFilepath, p2p) + binPath, err := os.Executable() + if err != nil { + return xerrors.Errorf("Failed to get executable path: %w", err) + } + + newConfig := makeNewConfigs(binPath, user.Username, workspacesWithProviders, privateKeyFilepath, p2p) err = os.MkdirAll(filepath.Dir(*configpath), os.ModePerm) if err != nil { @@ -193,7 +198,7 @@ func writeSSHKey(ctx context.Context, client coder.Client, privateKeyPath string return ioutil.WriteFile(privateKeyPath, []byte(key.PrivateKey), 0600) } -func makeNewConfigs(userName string, workspaces []coderutil.WorkspaceWithWorkspaceProvider, privateKeyFilepath string, p2p bool) string { +func makeNewConfigs(binPath, userName string, workspaces []coderutil.WorkspaceWithWorkspaceProvider, privateKeyFilepath string, p2p bool) string { newConfig := fmt.Sprintf("\n%s\n%s\n\n", sshStartToken, sshStartMessage) sort.Slice(workspaces, func(i, j int) bool { return workspaces[i].Workspace.Name < workspaces[j].Workspace.Name }) @@ -213,24 +218,24 @@ func makeNewConfigs(userName string, workspaces []coderutil.WorkspaceWithWorkspa } useTunnel := workspace.WorkspaceProvider.BuiltIn && p2p - newConfig += makeSSHConfig(u.Host, userName, workspace.Workspace.Name, privateKeyFilepath, useTunnel) + newConfig += makeSSHConfig(binPath, u.Host, userName, workspace.Workspace.Name, privateKeyFilepath, useTunnel) } newConfig += fmt.Sprintf("\n%s\n", sshEndToken) return newConfig } -func makeSSHConfig(host, userName, workspaceName, privateKeyFilepath string, tunnel bool) string { +func makeSSHConfig(binPath, host, userName, workspaceName, privateKeyFilepath string, tunnel bool) string { if tunnel { return fmt.Sprintf( `Host coder.%s HostName coder.%s - ProxyCommand coder tunnel %s 12213 stdio + ProxyCommand %s tunnel %s 12213 stdio StrictHostKeyChecking no ConnectTimeout=0 IdentitiesOnly yes IdentityFile="%s" -`, workspaceName, workspaceName, workspaceName, privateKeyFilepath) +`, workspaceName, workspaceName, binPath, workspaceName, privateKeyFilepath) } return fmt.Sprintf( From d86b78ceff28f57db03bc25237e5faf1bee5010a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 May 2021 13:21:04 -0700 Subject: [PATCH 041/128] chore: bump github.com/google/go-cmp from 0.5.5 to 0.5.6 (#359) Bumps [github.com/google/go-cmp](https://github.com/google/go-cmp) from 0.5.5 to 0.5.6. - [Release notes](https://github.com/google/go-cmp/releases) - [Commits](https://github.com/google/go-cmp/compare/v0.5.5...v0.5.6) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 10e1aca3..f3cdb705 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( cdr.dev/wsep v0.0.0-20200728013649-82316a09813f github.com/briandowns/spinner v1.12.0 github.com/fatih/color v1.10.0 - github.com/google/go-cmp v0.5.5 + github.com/google/go-cmp v0.5.6 github.com/gorilla/websocket v1.4.2 github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f diff --git a/go.sum b/go.sum index 80622413..5f5845d4 100644 --- a/go.sum +++ b/go.sum @@ -138,8 +138,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= From 0d648e8a43d5e3047e6dde1dad0d343fd4c3b1cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 May 2021 13:58:05 -0700 Subject: [PATCH 042/128] chore: bump github.com/fatih/color from 1.10.0 to 1.12.0 (#358) Bumps [github.com/fatih/color](https://github.com/fatih/color) from 1.10.0 to 1.12.0. - [Release notes](https://github.com/fatih/color/releases) - [Commits](https://github.com/fatih/color/compare/v1.10.0...v1.12.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f3cdb705..ad1139dd 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( cdr.dev/slog v1.4.0 cdr.dev/wsep v0.0.0-20200728013649-82316a09813f github.com/briandowns/spinner v1.12.0 - github.com/fatih/color v1.10.0 + github.com/fatih/color v1.12.0 github.com/google/go-cmp v0.5.6 github.com/gorilla/websocket v1.4.2 github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 diff --git a/go.sum b/go.sum index 5f5845d4..156531b9 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= +github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= From 2d470e4d1a02d88864e9779a7d782949c9f3dbcf Mon Sep 17 00:00:00 2001 From: Nathan Potter Date: Wed, 26 May 2021 19:42:54 -0500 Subject: [PATCH 043/128] Adjust HTTPError to read response body before it is closed (#361) * Adjust HTTPError to read response body before it is closed * Remove response size limitation --- coder-sdk/activity.go | 2 +- coder-sdk/error.go | 40 +++++++++++++++++++++++++++++++--------- coder-sdk/request.go | 2 +- coder-sdk/ws.go | 2 +- internal/cmd/auth.go | 2 +- internal/cmd/errors.go | 2 +- wsnet/dial.go | 4 +--- wsnet/listen.go | 4 +--- 8 files changed, 38 insertions(+), 20 deletions(-) diff --git a/coder-sdk/activity.go b/coder-sdk/activity.go index 6d564efb..23c75abd 100644 --- a/coder-sdk/activity.go +++ b/coder-sdk/activity.go @@ -21,7 +21,7 @@ func (c *DefaultClient) PushActivity(ctx context.Context, source, workspaceID st } if resp.StatusCode != http.StatusOK { - return bodyError(resp) + return NewHTTPError(resp) } return nil } diff --git a/coder-sdk/error.go b/coder-sdk/error.go index 2973046a..9e1645d0 100644 --- a/coder-sdk/error.go +++ b/coder-sdk/error.go @@ -1,8 +1,10 @@ package coder import ( + "bytes" "encoding/json" "fmt" + "io" "net/http" "golang.org/x/xerrors" @@ -29,11 +31,31 @@ type APIErrorMsg struct { Details json.RawMessage `json:"details"` } +// NewHTTPError reads the response body and stores metadata +// about the response in order to be unpacked into +// an *APIError. +func NewHTTPError(resp *http.Response) *HTTPError { + var buf bytes.Buffer + _, err := io.Copy(&buf, resp.Body) + if err != nil { + return &HTTPError{ + cachedErr: err, + } + } + return &HTTPError{ + url: resp.Request.URL.String(), + statusCode: resp.StatusCode, + body: buf.Bytes(), + } +} + // HTTPError represents an error from the Coder API. type HTTPError struct { - *http.Response - cached *APIError - cachedErr error + url string + statusCode int + body []byte + cached *APIError + cachedErr error } // Payload decode the response body into the standard error structure. The `details` @@ -46,7 +68,7 @@ func (e *HTTPError) Payload() (*APIError, error) { // Try to decode the payload as an error, if it fails or if there is no error message, // return the response URL with the status. - if err := json.NewDecoder(e.Response.Body).Decode(&msg); err != nil { + if err := json.Unmarshal(e.body, &msg); err != nil { e.cachedErr = err return nil, err } @@ -55,16 +77,16 @@ func (e *HTTPError) Payload() (*APIError, error) { return &msg, nil } +func (e *HTTPError) StatusCode() int { + return e.statusCode +} + func (e *HTTPError) Error() string { apiErr, err := e.Payload() if err != nil || apiErr.Err.Msg == "" { - return fmt.Sprintf("%s: %d %s", e.Request.URL, e.StatusCode, e.Status) + return fmt.Sprintf("%s: %d %s", e.url, e.statusCode, http.StatusText(e.statusCode)) } // If the payload was a in the expected error format with a message, include it. return apiErr.Err.Msg } - -func bodyError(resp *http.Response) error { - return &HTTPError{Response: resp} -} diff --git a/coder-sdk/request.go b/coder-sdk/request.go index a49ddda1..d8f8bb76 100644 --- a/coder-sdk/request.go +++ b/coder-sdk/request.go @@ -114,7 +114,7 @@ func (c *DefaultClient) requestBody(ctx context.Context, method, path string, in // Responses in the 100 are handled by the http lib, in the 200 range, we have a success. // Consider anything at or above 300 to be an error. if resp.StatusCode > 299 { - return fmt.Errorf("unexpected status code %d: %w", resp.StatusCode, bodyError(resp)) + return fmt.Errorf("unexpected status code %d: %w", resp.StatusCode, NewHTTPError(resp)) } // If we expect a payload, process it as json. diff --git a/coder-sdk/ws.go b/coder-sdk/ws.go index 6f6a920f..89cb28e8 100644 --- a/coder-sdk/ws.go +++ b/coder-sdk/ws.go @@ -26,7 +26,7 @@ func (c *DefaultClient) dialWebsocket(ctx context.Context, path string, options conn, resp, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{HTTPHeader: headers}) if err != nil { if resp != nil { - return nil, bodyError(resp) + return nil, NewHTTPError(resp) } return nil, err } diff --git a/internal/cmd/auth.go b/internal/cmd/auth.go index 56aaef80..2c59611a 100644 --- a/internal/cmd/auth.go +++ b/internal/cmd/auth.go @@ -66,7 +66,7 @@ func newClient(ctx context.Context, checkVersion bool) (coder.Client, error) { if err != nil { var he *coder.HTTPError if xerrors.As(err, &he) { - if he.StatusCode == http.StatusUnauthorized { + if he.StatusCode() == http.StatusUnauthorized { return nil, xerrors.Errorf("not authenticated: try running \"coder login`\"") } } diff --git a/internal/cmd/errors.go b/internal/cmd/errors.go index e202037e..dce13918 100644 --- a/internal/cmd/errors.go +++ b/internal/cmd/errors.go @@ -61,7 +61,7 @@ func handleAPIError(origError error) error { return origError } - return clog.Error(fmt.Sprintf("Precondition Error : Status Code=%d", httpError.StatusCode), + return clog.Error(fmt.Sprintf("Precondition Error : Status Code=%d", httpError.StatusCode()), p.Message, clog.BlankLine, clog.Tipf(p.Solution)) diff --git a/wsnet/dial.go b/wsnet/dial.go index c8f0f7e3..6900bacf 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -24,9 +24,7 @@ func DialWebsocket(ctx context.Context, broker string, iceServers []webrtc.ICESe defer func() { _ = resp.Body.Close() }() - return nil, &coder.HTTPError{ - Response: resp, - } + return nil, coder.NewHTTPError(resp) } return nil, fmt.Errorf("dial websocket: %w", err) } diff --git a/wsnet/listen.go b/wsnet/listen.go index c11df79c..92f7671f 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -81,9 +81,7 @@ func (l *listener) dial(ctx context.Context) (<-chan error, error) { conn, resp, err := websocket.Dial(ctx, l.broker, nil) if err != nil { if resp != nil { - return nil, &coder.HTTPError{ - Response: resp, - } + return nil, coder.NewHTTPError(resp) } return nil, err } From 765c0dd20270c21e25b68acfeb23dba84cb10681 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 May 2021 11:13:00 -0700 Subject: [PATCH 044/128] chore: bump github.com/briandowns/spinner from 1.12.0 to 1.13.0 (#362) Bumps [github.com/briandowns/spinner](https://github.com/briandowns/spinner) from 1.12.0 to 1.13.0. - [Release notes](https://github.com/briandowns/spinner/releases) - [Commits](https://github.com/briandowns/spinner/compare/v1.12...v1.13.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ad1139dd..3823f149 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.14 require ( cdr.dev/slog v1.4.0 cdr.dev/wsep v0.0.0-20200728013649-82316a09813f - github.com/briandowns/spinner v1.12.0 + github.com/briandowns/spinner v1.13.0 github.com/fatih/color v1.12.0 github.com/google/go-cmp v0.5.6 github.com/gorilla/websocket v1.4.2 diff --git a/go.sum b/go.sum index 156531b9..c11f192d 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/briandowns/spinner v1.12.0 h1:72O0PzqGJb6G3KgrcIOtL/JAGGZ5ptOMCn9cUHmqsmw= -github.com/briandowns/spinner v1.12.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= +github.com/briandowns/spinner v1.13.0 h1:q/Y9LtpwtvL0CRzXrAMj0keVXqNhBYUFg6tBOUiY8ek= +github.com/briandowns/spinner v1.13.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= From 544f276f973c2f285642231fee4ca08eea9f3136 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 8 Jun 2021 02:42:00 +1000 Subject: [PATCH 045/128] wsnet dial policy (#364) * wsnet dial policy * Add wsnet address/policy tests * Add op to not permitted errors * Add Code to dial response struct * fixup! Add Code to dial response struct * fixup! Add Code to dial response struct --- wsnet/dial.go | 23 +++-- wsnet/listen.go | 146 ++++++++++++++++----------- wsnet/proto.go | 118 ++++++++++++++++++++-- wsnet/proto_test.go | 235 ++++++++++++++++++++++++++++++++++++++++++++ wsnet/rtc.go | 2 +- 5 files changed, 447 insertions(+), 77 deletions(-) create mode 100644 wsnet/proto_test.go diff --git a/wsnet/dial.go b/wsnet/dial.go index 6900bacf..23581eaf 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -67,7 +67,7 @@ func Dial(conn net.Conn, iceServers []webrtc.ICEServer) (*Dialer, error) { return nil, fmt.Errorf("set local offer: %w", err) } - offerMessage, err := json.Marshal(&protoMessage{ + offerMessage, err := json.Marshal(&BrokerMessage{ Offer: &offer, Servers: iceServers, }) @@ -124,7 +124,7 @@ func (d *Dialer) negotiate() (err error) { }() for { - var msg protoMessage + var msg BrokerMessage err = decoder.Decode(&msg) if errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe) { break @@ -218,24 +218,23 @@ func (d *Dialer) DialContext(ctx context.Context, network, address string) (net. errCh := make(chan error) go func() { - var init dialChannelMessage - err = json.NewDecoder(rw).Decode(&init) + var res DialChannelResponse + err = json.NewDecoder(rw).Decode(&res) if err != nil { - errCh <- fmt.Errorf("read init: %w", err) + errCh <- fmt.Errorf("read dial response: %w", err) return } - if init.Err == "" { + if res.Err == "" { close(errCh) return } - err := errors.New(init.Err) - if init.Net != "" { - errCh <- &net.OpError{ - Op: init.Op, - Net: init.Net, + err := errors.New(res.Err) + if res.Code == CodeDialErr { + err = &net.OpError{ + Op: res.Op, + Net: res.Net, Err: err, } - return } errCh <- err }() diff --git a/wsnet/listen.go b/wsnet/listen.go index 92f7671f..1496e19c 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "net" - "strings" "sync" "time" @@ -18,8 +17,26 @@ import ( "cdr.dev/coder-cli/coder-sdk" ) +// Codes for DialChannelResponse. +const ( + CodeDialErr = "dial_error" + CodePermissionErr = "permission_error" + CodeBadAddressErr = "bad_address_error" +) + var connectionRetryInterval = time.Second +// DialChannelResponse is used to notify a dial channel of a +// listening state. Modeled after net.OpError, and marshalled +// to that if Net is not "". +type DialChannelResponse struct { + Code string + Err string + // Fields are set if the code is CodeDialErr. + Net string + Op string +} + // Listen connects to the broker proxies connections to the local net. // Close will end all RTC connections. func Listen(ctx context.Context, broker string) (io.Closer, error) { @@ -124,7 +141,7 @@ func (l *listener) negotiate(conn net.Conn) { // Sends the error provided then closes the connection. // If RTC isn't connected, we'll close it. closeError = func(err error) { - d, _ := json.Marshal(&protoMessage{ + d, _ := json.Marshal(&BrokerMessage{ Error: err.Error(), }) _, _ = conn.Write(d) @@ -139,7 +156,7 @@ func (l *listener) negotiate(conn net.Conn) { ) for { - var msg protoMessage + var msg BrokerMessage err = decoder.Decode(&msg) if err != nil { closeError(err) @@ -190,7 +207,7 @@ func (l *listener) negotiate(conn net.Conn) { l.connClosersMut.Lock() l.connClosers = append(l.connClosers, rtc) l.connClosersMut.Unlock() - rtc.OnDataChannel(l.handle) + rtc.OnDataChannel(l.handle(msg)) err = rtc.SetRemoteDescription(*msg.Offer) if err != nil { closeError(fmt.Errorf("apply offer: %w", err)) @@ -208,7 +225,7 @@ func (l *listener) negotiate(conn net.Conn) { } flushCandidates() - data, err := json.Marshal(&protoMessage{ + data, err := json.Marshal(&BrokerMessage{ Answer: rtc.LocalDescription(), }) if err != nil { @@ -233,70 +250,89 @@ func (l *listener) negotiate(conn net.Conn) { } } -func (l *listener) handle(dc *webrtc.DataChannel) { - if dc.Protocol() == controlChannel { - // The control channel handles pings. +func (l *listener) handle(msg BrokerMessage) func(dc *webrtc.DataChannel) { + return func(dc *webrtc.DataChannel) { + if dc.Protocol() == controlChannel { + // The control channel handles pings. + dc.OnOpen(func() { + rw, err := dc.Detach() + if err != nil { + return + } + // We'll read and write back a single byte for ping/pongin'. + d := make([]byte, 1) + for { + _, err = rw.Read(d) + if errors.Is(err, io.EOF) { + return + } + if err != nil { + continue + } + _, _ = rw.Write(d) + } + }) + return + } + dc.OnOpen(func() { rw, err := dc.Detach() if err != nil { return } - // We'll read and write back a single byte for ping/pongin'. - d := make([]byte, 1) - for { - _, err = rw.Read(d) - if errors.Is(err, io.EOF) { + + var init DialChannelResponse + sendInitMessage := func() { + initData, err := json.Marshal(&init) + if err != nil { + rw.Close() return } + _, err = rw.Write(initData) if err != nil { - continue + return + } + if init.Err != "" { + // If an error occurred, we're safe to close the connection. + dc.Close() + return } - _, _ = rw.Write(d) } - }) - return - } - dc.OnOpen(func() { - rw, err := dc.Detach() - if err != nil { - return - } - parts := strings.SplitN(dc.Protocol(), ":", 2) - network := parts[0] - addr := parts[1] + network, addr, err := msg.getAddress(dc.Protocol()) + if err != nil { + init.Code = CodeBadAddressErr + init.Err = err.Error() + var policyErr notPermittedByPolicyErr + if errors.As(err, &policyErr) { + init.Code = CodePermissionErr + } + sendInitMessage() + return + } - var init dialChannelMessage - conn, err := net.Dial(network, addr) - if err != nil { - init.Err = err.Error() - if op, ok := err.(*net.OpError); ok { - init.Net = op.Net - init.Op = op.Op + conn, err := net.Dial(network, addr) + if err != nil { + init.Code = CodeDialErr + init.Err = err.Error() + if op, ok := err.(*net.OpError); ok { + init.Net = op.Net + init.Op = op.Op + } } - } - initData, err := json.Marshal(&init) - if err != nil { - rw.Close() - return - } - _, err = rw.Write(initData) - if err != nil { - return - } - if init.Err != "" { - // If an error occurred, we're safe to close the connection. - dc.Close() - return - } - defer conn.Close() - defer dc.Close() + sendInitMessage() + if init.Err != "" { + return + } + defer conn.Close() + defer dc.Close() - go func() { - _, _ = io.Copy(rw, conn) - }() - _, _ = io.Copy(conn, rw) - }) + go func() { + _, _ = io.Copy(rw, conn) + }() + _, _ = io.Copy(conn, rw) + }) + } } // Close closes the broker socket and all created RTC connections. diff --git a/wsnet/proto.go b/wsnet/proto.go index cbe3ac82..754fffac 100644 --- a/wsnet/proto.go +++ b/wsnet/proto.go @@ -1,10 +1,45 @@ package wsnet import ( + "fmt" + "math/bits" + "net" + "strconv" + "strings" + "github.com/pion/webrtc/v3" ) -// protoMessage is used for brokering a dialer and listener. +// DialPolicy a single network + address + port combinations that a connection +// is permitted to use. +type DialPolicy struct { + // If network is empty, it applies to all networks. + Network string `json:"network"` + // Host is the IP or hostname of the address. It should not contain the + // port.If empty, it applies to all hosts. "localhost", [::1], and any IPv4 + // address under "127.0.0.0/8" can be used interchangeably. + Host string `json:"address"` + // If port is 0, it applies to all ports. + Port uint16 `json:"port"` +} + +// permits checks if a DialPolicy permits a specific network + host + port +// combination. The host must be put through normalizeHost first. +func (p DialPolicy) permits(network, host string, port uint16) bool { + if p.Network != "" && p.Network != network { + return false + } + if p.Host != "" && canonicalizeHost(p.Host) != host { + return false + } + if p.Port != 0 && p.Port != port { + return false + } + + return true +} + +// BrokerMessage is used for brokering a dialer and listener. // // Dialers initiate an exchange by providing an Offer, // along with a list of ICE servers for the listener to @@ -12,10 +47,13 @@ import ( // // The listener should respond with an offer, then both // sides can begin exchanging candidates. -type protoMessage struct { +type BrokerMessage struct { // Dialer -> Listener Offer *webrtc.SessionDescription `json:"offer"` Servers []webrtc.ICEServer `json:"servers"` + // Policies denote which addresses the client can dial. If empty or nil, all + // addresses are permitted. + Policies []DialPolicy `json:"ports"` // Listener -> Dialer Error string `json:"error"` @@ -25,11 +63,73 @@ type protoMessage struct { Candidate string `json:"candidate"` } -// dialChannelMessage is used to notify a dial channel of a -// listening state. Modeled after net.OpError, and marshalled -// to that if Net is not "". -type dialChannelMessage struct { - Err string - Net string - Op string +// getAddress parses the data channel's protocol into an address suitable for +// net.Dial. It also verifies that the BrokerMessage permits connecting to said +// address. +func (msg BrokerMessage) getAddress(protocol string) (netwk, addr string, err error) { + parts := strings.SplitN(protocol, ":", 2) + if len(parts) != 2 { + return "", "", fmt.Errorf("invalid dial address: %v", protocol) + } + host, port, err := net.SplitHostPort(parts[1]) + if err != nil { + return "", "", fmt.Errorf("invalid dial address: %v", protocol) + } + + var ( + network = parts[0] + normalHost = canonicalizeHost(host) + // Still return the original host value, not the canonical value. + fullAddr = net.JoinHostPort(host, port) + ) + if network == "" { + return "", "", fmt.Errorf("invalid dial address %q network: %v", protocol, network) + } + if host == "" { + return "", "", fmt.Errorf("invalid dial address %q host: %v", protocol, host) + } + + portParsed, err := strconv.Atoi(port) + if err != nil || portParsed < 0 || bits.Len(uint(portParsed)) > 16 { + return "", "", fmt.Errorf("invalid dial address %q port: %v", protocol, port) + } + if len(msg.Policies) == 0 { + return network, fullAddr, nil + } + + portParsedU16 := uint16(portParsed) + for _, p := range msg.Policies { + if p.permits(network, normalHost, portParsedU16) { + return network, fullAddr, nil + } + } + + return "", "", fmt.Errorf("connections are not permitted to %q by policy", protocol) +} + +// canonicalizeHost converts all representations of "localhost" to "localhost". +func canonicalizeHost(addr string) string { + addr = strings.TrimPrefix(addr, "[") + addr = strings.TrimSuffix(addr, "]") + + ip := net.ParseIP(addr) + if ip == nil { + return addr + } + + if ip.IsLoopback() { + return "localhost" + } + return addr +} + +type notPermittedByPolicyErr struct { + protocol string +} + +var _ error = notPermittedByPolicyErr{} + +// Error implements error. +func (e notPermittedByPolicyErr) Error() string { + return fmt.Sprintf("connections are not permitted to %q by policy", e.protocol) } diff --git a/wsnet/proto_test.go b/wsnet/proto_test.go new file mode 100644 index 00000000..89999f6b --- /dev/null +++ b/wsnet/proto_test.go @@ -0,0 +1,235 @@ +package wsnet + +import ( + "fmt" + "testing" + + "cdr.dev/slog/sloggers/slogtest/assert" +) + +func Test_BrokerMessage(t *testing.T) { + t.Run("getAddress", func(t *testing.T) { + t.Run("OK", func(t *testing.T) { + var ( + msg = BrokerMessage{ + Policies: nil, + } + network = "tcp" + addr = "localhost:1234" + ) + + protocol := formatAddress(network, addr) + gotNetwork, gotAddr, err := msg.getAddress(protocol) + assert.Success(t, "got address", err) + assert.Equal(t, "networks equal", network, gotNetwork) + assert.Equal(t, "addresses equal", addr, gotAddr) + + msg.Policies = []DialPolicy{} + gotNetwork, gotAddr, err = msg.getAddress(protocol) + assert.Success(t, "got address", err) + assert.Equal(t, "networks equal", network, gotNetwork) + assert.Equal(t, "addresses equal", addr, gotAddr) + }) + + t.Run("InvalidProtocol", func(t *testing.T) { + cases := []struct { + protocol string + errContains string + }{ + { + protocol: "", + errContains: "invalid", + }, + { + protocol: "a:b", + errContains: "invalid", + }, + { + protocol: "a:b:c:d", + errContains: "invalid", + }, + { + protocol: ":localhost:1234", + errContains: "network", + }, + { + protocol: "tcp::1234", + errContains: "host", + }, + { + protocol: "tcp:localhost:", + errContains: "port", + }, + { + protocol: "tcp:localhost:asdf", + errContains: "port", + }, + { + protocol: "tcp:localhost:-1", + errContains: "port", + }, + { + // Overflow uint16. + protocol: fmt.Sprintf("tcp:localhost:%v", uint(1)<<16), + errContains: "port", + }, + } + + var msg BrokerMessage + for i, c := range cases { + amsg := fmt.Sprintf("case %v %q: ", i, c) + gotNetwork, gotAddr, err := msg.getAddress(c.protocol) + assert.Error(t, amsg+"successfully got invalid address", err) + assert.ErrorContains(t, fmt.Sprintf("%verr contains %q", amsg, c.errContains), err, c.errContains) + assert.Equal(t, amsg+"empty network", "", gotNetwork) + assert.Equal(t, amsg+"empty address", "", gotAddr) + } + }) + + t.Run("ChecksPolicies", func(t *testing.T) { + // ok == true tests automatically have a bunch of non-matching dial + // policies injected in front of them. + cases := []struct { + network string + host string + port uint16 + policy DialPolicy + ok bool + }{ + { + network: "tcp", + host: "localhost", + port: 1234, + policy: dialPolicy("tcp", "localhost", 1234), + ok: true, + }, + { + network: "tcp", + host: "localhost", + port: 1234, + policy: dialPolicy("udp", "example.com", 51), + ok: false, + }, + // Network checks. + { + network: "tcp", + host: "localhost", + port: 1234, + policy: dialPolicy("", "localhost", 1234), + ok: true, + }, + { + network: "tcp", + host: "localhost", + port: 1234, + policy: dialPolicy("udp", "localhost", 1234), + ok: false, + }, + // Host checks. + { + network: "tcp", + host: "localhost", + port: 1234, + policy: dialPolicy("tcp", "", 1234), + ok: true, + }, + { + network: "tcp", + host: "localhost", + port: 1234, + policy: dialPolicy("tcp", "127.0.0.1", 1234), + ok: true, + }, + { + network: "tcp", + host: "127.0.0.1", + port: 1234, + policy: dialPolicy("tcp", "127.1.2.3", 1234), + ok: true, + }, + { + network: "tcp", + host: "[::1]", + port: 1234, + policy: dialPolicy("tcp", "127.1.2.3", 1234), + ok: true, + }, + { + network: "tcp", + host: "localhost", + port: 1234, + policy: dialPolicy("tcp", "example.com", 1234), + ok: false, + }, + { + network: "tcp", + host: "example.com", + port: 1234, + policy: dialPolicy("tcp", "localhost", 1234), + ok: false, + }, + // Port checks. + { + network: "tcp", + host: "localhost", + port: 1234, + policy: dialPolicy("tcp", "localhost", 5678), + ok: false, + }, + { + network: "tcp", + host: "localhost", + port: 1234, + policy: dialPolicy("tcp", "localhost", 0), + ok: true, + }, + } + + for i, c := range cases { + var ( + amsg = fmt.Sprintf("case %v '%+v': ", i, c) + msg = BrokerMessage{ + Policies: []DialPolicy{c.policy}, + } + ) + + // Add nonsense policies before the matching policy. + if c.ok { + msg.Policies = []DialPolicy{ + dialPolicy("asdf", "localhost", 1234), + dialPolicy("tcp", "asdf", 1234), + dialPolicy("tcp", "localhost", 17208), + c.policy, + } + } + + // Test DialPolicy. + assert.Equal(t, amsg+"policy matches", c.ok, c.policy.permits(c.network, canonicalizeHost(c.host), c.port)) + + // Test BrokerMessage. + protocol := formatAddress(c.network, fmt.Sprintf("%v:%v", c.host, c.port)) + gotNetwork, gotAddr, err := msg.getAddress(protocol) + if c.ok { + assert.Success(t, amsg, err) + } else { + assert.Error(t, amsg+"successfully got invalid address", err) + assert.ErrorContains(t, amsg+"err contains 'not permitted'", err, "not permitted") + assert.Equal(t, amsg+"empty network", "", gotNetwork) + assert.Equal(t, amsg+"empty address", "", gotAddr) + } + } + }) + }) +} + +func formatAddress(network, addr string) string { + return fmt.Sprintf("%v:%v", network, addr) +} + +func dialPolicy(network, host string, port uint16) DialPolicy { + return DialPolicy{ + Network: network, + Host: host, + Port: port, + } +} diff --git a/wsnet/rtc.go b/wsnet/rtc.go index 9c07663d..f5c7c5f3 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -186,7 +186,7 @@ func proxyICECandidates(conn *webrtc.PeerConnection, w io.Writer) func() { queue = []*webrtc.ICECandidate{} flushed = false write = func(i *webrtc.ICECandidate) { - b, _ := json.Marshal(&protoMessage{ + b, _ := json.Marshal(&BrokerMessage{ Candidate: i.ToJSON().Candidate, }) _, _ = w.Write(b) From 177ee840e8a828ae6ec7cef4f58e87bb8325a7bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 17:16:11 -0700 Subject: [PATCH 046/128] chore: bump cdr.dev/slog from 1.4.0 to 1.4.1 (#367) Bumps [cdr.dev/slog](https://github.com/cdr/slog) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/cdr/slog/releases) - [Commits](https://github.com/cdr/slog/compare/v1.4.0...v1.4.1) --- updated-dependencies: - dependency-name: cdr.dev/slog dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +- go.sum | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 290 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 3823f149..10243806 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module cdr.dev/coder-cli go 1.14 require ( - cdr.dev/slog v1.4.0 + cdr.dev/slog v1.4.1 cdr.dev/wsep v0.0.0-20200728013649-82316a09813f github.com/briandowns/spinner v1.13.0 github.com/fatih/color v1.12.0 @@ -21,8 +21,8 @@ require ( github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.1.3 - golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 - golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 diff --git a/go.sum b/go.sum index c11f192d..eeed5d40 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ cdr.dev/slog v1.3.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns= -cdr.dev/slog v1.4.0 h1:tLXQJ/hZ5Q051h0MBHSd2Ha8xzdXj7CjtzmG/8dUvUk= -cdr.dev/slog v1.4.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns= +cdr.dev/slog v1.4.1 h1:Q8+X63m8/WB4geelMTDO8t4CTwVh1f7+5Cxi7kS/SZg= +cdr.dev/slog v1.4.1/go.mod h1:O76C6gZJxa5HK1SXMrjd48V2kJxYZKFRTcFfn/V9OhA= cdr.dev/wsep v0.0.0-20200728013649-82316a09813f h1:WnTUINBwXE11xjp5nTVt+H2qB2/KEymos1jKMcppG9U= cdr.dev/wsep v0.0.0-20200728013649-82316a09813f/go.mod h1:2VKClUml3gfmLez0gBxTJIjSKszpQotc2ZqPdApfK/Y= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -10,13 +10,40 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.49.0 h1:CH+lkubJzcPYB1Ggupcq0+k8Ni2ILdG2lYjDIgavDBQ= cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0 h1:bAMqZidYkmIsUqe6PtkEPT7Q+vfizScn+jfNA6jwK9c= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -26,12 +53,14 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= -github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw= github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY= +github.com/alecthomas/chroma v0.9.1 h1:cBmvQqRImzR5aWqdMxYZByND4S7BCS/g0svZb28h0Dc= +github.com/alecthomas/chroma v0.9.1/go.mod h1:eMuEnpA18XbG/WhOWtCzJHS7WqEtDAI+HxdwoW0nVSk= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= @@ -55,6 +84,9 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -73,9 +105,15 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= @@ -88,6 +126,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -109,28 +149,40 @@ github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE= github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -138,13 +190,34 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -183,6 +256,8 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 h1:Y4V+SFe7d3iH+9pJCoeWIOS5/xBJIFsltS7E+KJSsJY= github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -191,6 +266,7 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= @@ -219,6 +295,7 @@ github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEX github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -358,15 +435,23 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.coder.com/cli v0.4.0/go.mod h1:hRTOURCR3LJF1FRW9arecgrzX+AHG7mfYMwThPIgq+w= go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512 h1:DjCS6dRQh+1PlfiBmnabxfdrzenb0tAwJqFxDEH/s9g= go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512/go.mod h1:83JsYgXYv0EOaXjIMnaZ1Fl6ddNB3fJnDZ/8845mUJ8= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= @@ -385,6 +470,11 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -394,11 +484,22 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -414,27 +515,61 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210420210106-798c2154c571 h1:Q6Bg8xzKzpFPU4Oi1sBnBTHBwlMsLeEXpu4hYBY8rAg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210420210106-798c2154c571/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -453,26 +588,56 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe h1:WdX7u8s3yOigWAhHEaDl8r9G+4XwFQEQFtBMYyN+kXQ= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -497,9 +662,40 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -511,10 +707,29 @@ google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEn google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -525,21 +740,74 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1 h1:aQktFqmDE2yjveXJlVIfslDFmFnUXSqG0i6KRcJAeMc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -565,7 +833,11 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From f85e0547ebb020eac38a1bfd748b97d465a573d6 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 15 Jun 2021 16:31:31 -0400 Subject: [PATCH 047/128] feat: Add Benchmarks to test WebRTC connections (#368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add Benchmarks to test WebRTC connections * Isolate race condition further * Add race condition 😥 * Add benchmark step * Fix linting * Suppress TURN output * Remove bench action --- go.mod | 1 + wsnet/conn.go | 55 +++++++++++++++++++++++++++++++++++- wsnet/dial.go | 7 +++-- wsnet/dial_test.go | 69 +++++++++++++++++++++++++++++++++++++++++++++ wsnet/listen.go | 18 ++++++++---- wsnet/rtc.go | 4 +++ wsnet/wsnet_test.go | 6 +++- 7 files changed, 151 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 10243806..02e3134a 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/pion/datachannel v1.4.21 github.com/pion/dtls/v2 v2.0.9 github.com/pion/ice/v2 v2.1.7 + github.com/pion/logging v0.2.2 github.com/pion/turn/v2 v2.0.5 github.com/pion/webrtc/v3 v3.0.29 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 diff --git a/wsnet/conn.go b/wsnet/conn.go index 7e18723b..b5dea0a5 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -4,13 +4,22 @@ import ( "fmt" "net" "net/url" + "sync" "time" "github.com/pion/datachannel" + "github.com/pion/webrtc/v3" ) const ( httpScheme = "http" + + bufferedAmountLowThreshold uint64 = 512 * 1024 // 512 KB + maxBufferedAmount uint64 = 1024 * 1024 // 1 MB + // For some reason messages larger just don't work... + // This shouldn't be a huge deal for real-world usage. + // See: https://github.com/pion/datachannel/issues/59 + maxMessageLength = 32 * 1024 // 32 KB ) // TURNEndpoint returns the TURN address for a Coder baseURL. @@ -43,7 +52,30 @@ func ConnectEndpoint(baseURL *url.URL, workspace, token string) string { type conn struct { addr *net.UnixAddr + dc *webrtc.DataChannel rw datachannel.ReadWriteCloser + + sendMore chan struct{} + closedMutex sync.RWMutex + closed bool + + writeMutex sync.Mutex +} + +func (c *conn) init() { + c.sendMore = make(chan struct{}, 1) + c.dc.SetBufferedAmountLowThreshold(bufferedAmountLowThreshold) + c.dc.OnBufferedAmountLow(func() { + c.closedMutex.RLock() + defer c.closedMutex.RUnlock() + if c.closed { + return + } + select { + case c.sendMore <- struct{}{}: + default: + } + }) } func (c *conn) Read(b []byte) (n int, err error) { @@ -51,11 +83,32 @@ func (c *conn) Read(b []byte) (n int, err error) { } func (c *conn) Write(b []byte) (n int, err error) { + c.writeMutex.Lock() + defer c.writeMutex.Unlock() + if len(b) > maxMessageLength { + return 0, fmt.Errorf("outbound packet larger than maximum message size: %d", maxMessageLength) + } + if c.dc.BufferedAmount()+uint64(len(b)) >= maxBufferedAmount { + <-c.sendMore + } + // TODO (@kyle): There's an obvious race-condition here. + // This is an edge-case, as most-frequently data won't + // be pooled so synchronously, but is definitely possible. + // + // See: https://github.com/pion/sctp/issues/181 + time.Sleep(time.Microsecond) + return c.rw.Write(b) } func (c *conn) Close() error { - return c.rw.Close() + c.closedMutex.Lock() + defer c.closedMutex.Unlock() + if !c.closed { + c.closed = true + close(c.sendMore) + } + return c.dc.Close() } func (c *conn) LocalAddr() net.Addr { diff --git a/wsnet/dial.go b/wsnet/dial.go index 23581eaf..9debf47a 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -249,11 +249,14 @@ func (d *Dialer) DialContext(ctx context.Context, network, address string) (net. return nil, ctx.Err() } - return &conn{ + c := &conn{ addr: &net.UnixAddr{ Name: address, Net: network, }, + dc: dc, rw: rw, - }, nil + } + c.init() + return c, nil } diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 584c37d5..50bdd938 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -3,9 +3,11 @@ package wsnet import ( "bytes" "context" + "crypto/rand" "errors" "io" "net" + "strconv" "testing" "github.com/pion/webrtc/v3" @@ -160,3 +162,70 @@ func TestDial(t *testing.T) { } }) } + +func BenchmarkThroughput(b *testing.B) { + sizes := []int64{ + 4, + 16, + 128, + 256, + 1024, + 4096, + 16384, + 32768, + } + + listener, err := net.Listen("tcp", "0.0.0.0:0") + if err != nil { + b.Error(err) + return + } + go func() { + for { + conn, err := listener.Accept() + if err != nil { + b.Error(err) + return + } + go func() { + _, _ = io.Copy(io.Discard, conn) + }() + } + }() + connectAddr, listenAddr := createDumbBroker(b) + _, err = Listen(context.Background(), listenAddr) + if err != nil { + b.Error(err) + return + } + + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + if err != nil { + b.Error(err) + return + } + for _, size := range sizes { + size := size + bytes := make([]byte, size) + _, _ = rand.Read(bytes) + b.Run("Rand"+strconv.Itoa(int(size)), func(b *testing.B) { + b.SetBytes(size) + b.ReportAllocs() + + conn, err := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) + if err != nil { + b.Error(err) + return + } + defer conn.Close() + + for i := 0; i < b.N; i++ { + _, err := conn.Write(bytes) + if err != nil { + b.Error(err) + break + } + } + }) + } +} diff --git a/wsnet/listen.go b/wsnet/listen.go index 1496e19c..3a6735f0 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -311,7 +311,7 @@ func (l *listener) handle(msg BrokerMessage) func(dc *webrtc.DataChannel) { return } - conn, err := net.Dial(network, addr) + nc, err := net.Dial(network, addr) if err != nil { init.Code = CodeDialErr init.Err = err.Error() @@ -324,13 +324,21 @@ func (l *listener) handle(msg BrokerMessage) func(dc *webrtc.DataChannel) { if init.Err != "" { return } - defer conn.Close() - defer dc.Close() + // Must wrap the data channel inside this connection + // for buffering from the dialed endpoint to the client. + co := &conn{ + addr: nil, + dc: dc, + rw: rw, + } + co.init() + defer co.Close() + defer nc.Close() go func() { - _, _ = io.Copy(rw, conn) + _, _ = io.Copy(co, nc) }() - _, _ = io.Copy(conn, rw) + _, _ = io.Copy(nc, co) }) } } diff --git a/wsnet/rtc.go b/wsnet/rtc.go index f5c7c5f3..0605fa29 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -14,6 +14,7 @@ import ( "github.com/pion/dtls/v2" "github.com/pion/ice/v2" + "github.com/pion/logging" "github.com/pion/turn/v2" "github.com/pion/webrtc/v3" ) @@ -159,6 +160,9 @@ func newPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, erro se.SetSrflxAcceptanceMinWait(0) se.DetachDataChannels() se.SetICETimeouts(time.Second*5, time.Second*5, time.Second*2) + lf := logging.NewDefaultLoggerFactory() + lf.DefaultLogLevel = logging.LogLevelDisabled + se.LoggerFactory = lf // If one server is provided and we know it's TURN, we can set the // relay acceptable so the connection starts immediately. diff --git a/wsnet/wsnet_test.go b/wsnet/wsnet_test.go index 8452015d..fc14cd3c 100644 --- a/wsnet/wsnet_test.go +++ b/wsnet/wsnet_test.go @@ -20,13 +20,14 @@ import ( "cdr.dev/slog/sloggers/slogtest/assert" "github.com/hashicorp/yamux" "github.com/pion/ice/v2" + "github.com/pion/logging" "github.com/pion/turn/v2" "nhooyr.io/websocket" ) // createDumbBroker proxies sockets between /listen and /connect // to emulate an authenticated WebSocket pair. -func createDumbBroker(t *testing.T) (connectAddr string, listenAddr string) { +func createDumbBroker(t testing.TB) (connectAddr string, listenAddr string) { listener, err := net.Listen("tcp4", "127.0.0.1:0") if err != nil { t.Error(err) @@ -128,6 +129,8 @@ func createTURNServer(t *testing.T, server ice.SchemeType, pass string) string { }} } + lf := logging.NewDefaultLoggerFactory() + lf.DefaultLogLevel = logging.LogLevelDisabled srv, err := turn.NewServer(turn.ServerConfig{ PacketConnConfigs: pcListeners, ListenerConfigs: listeners, @@ -135,6 +138,7 @@ func createTURNServer(t *testing.T, server ice.SchemeType, pass string) string { AuthHandler: func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) { return turn.GenerateAuthKey(username, realm, pass), true }, + LoggerFactory: lf, }) if err != nil { t.Error(err) From dd614ec8f2f71f7ea5fb29de6e32f6ab8bf667c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jun 2021 09:13:53 -0700 Subject: [PATCH 048/128] chore: bump github.com/briandowns/spinner from 1.13.0 to 1.15.0 (#363) Bumps [github.com/briandowns/spinner](https://github.com/briandowns/spinner) from 1.13.0 to 1.15.0. - [Release notes](https://github.com/briandowns/spinner/releases) - [Commits](https://github.com/briandowns/spinner/compare/v1.13.0...v1.15.0) --- updated-dependencies: - dependency-name: github.com/briandowns/spinner dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 02e3134a..14c140be 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.14 require ( cdr.dev/slog v1.4.1 cdr.dev/wsep v0.0.0-20200728013649-82316a09813f - github.com/briandowns/spinner v1.13.0 + github.com/briandowns/spinner v1.15.0 github.com/fatih/color v1.12.0 github.com/google/go-cmp v0.5.6 github.com/gorilla/websocket v1.4.2 diff --git a/go.sum b/go.sum index eeed5d40..4ad25ea8 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/briandowns/spinner v1.13.0 h1:q/Y9LtpwtvL0CRzXrAMj0keVXqNhBYUFg6tBOUiY8ek= -github.com/briandowns/spinner v1.13.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= +github.com/briandowns/spinner v1.15.0 h1:L0jR0MYN7OAeMwpTzDZWIeqyDLXtTeJFxqoq+sL0VQM= +github.com/briandowns/spinner v1.15.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= From cdbfc48d4e3099ce639d9c54bd2cc07c7821aadb Mon Sep 17 00:00:00 2001 From: Lily Hoffman <78573475+lilshoff@users.noreply.github.com> Date: Mon, 21 Jun 2021 16:29:29 -0700 Subject: [PATCH 049/128] feat: support setting policy template via local file or restoring to default (#366) --- coder-sdk/interface.go | 3 + coder-sdk/workspace.go | 145 +++++++++++++++++++++++ docs/coder_workspaces.md | 1 + docs/coder_workspaces_policy-template.md | 32 +++++ internal/cmd/workspaces.go | 83 +++++++++++++ 5 files changed, 264 insertions(+) create mode 100644 docs/coder_workspaces_policy-template.md diff --git a/coder-sdk/interface.go b/coder-sdk/interface.go index 47ed28c3..e439ca32 100644 --- a/coder-sdk/interface.go +++ b/coder-sdk/interface.go @@ -238,4 +238,7 @@ type Client interface { // RenameWorkspaceProvider changes an existing providers name field. RenameWorkspaceProvider(ctx context.Context, id string, name string) error + + // SetPolicyTemplate sets the workspace policy template + SetPolicyTemplate(ctx context.Context, templateID string, templateScope TemplateScope, dryRun bool) (*SetPolicyTemplateResponse, error) } diff --git a/coder-sdk/workspace.go b/coder-sdk/workspace.go index e48d4bc7..b6071b5a 100644 --- a/coder-sdk/workspace.go +++ b/coder-sdk/workspace.go @@ -2,9 +2,11 @@ package coder import ( "context" + "fmt" "io" "net/http" "net/url" + "strings" "time" "cdr.dev/wsep" @@ -364,3 +366,146 @@ func (c *DefaultClient) WorkspacesByWorkspaceProvider(ctx context.Context, wpID } return workspaces, nil } + +const ( + // SkipTemplateOrg allows skipping checks on organizations. + SkipTemplateOrg = "SKIP_ORG" +) + +type TemplateScope string + +const ( + // TemplateScopeSite is the scope for a site wide policy template. + TemplateScopeSite = "site" +) + +type SetPolicyTemplateRequest struct { + TemplateID string `json:"template_id"` + Type string `json:"type"` // site, org +} + +type SetPolicyTemplateResponse struct { + MergeConflicts []*WorkspaceTemplateMergeConflict `json:"merge_conflicts"` +} + +type WorkspaceTemplateMergeConflict struct { + WorkspaceID string `json:"workspace_id"` + CurrentTemplateWarnings []string `json:"current_template_warnings"` + CurrentTemplateError *TplError `json:"current_template_errors"` + LatestTemplateWarnings []string `json:"latest_template_warnings"` + LatestTemplateError *TplError `json:"latest_template_errors"` + CurrentTemplateIsLatest bool `json:"current_template_is_latest"` + Message string `json:"message"` +} + +func (mc WorkspaceTemplateMergeConflict) String() string { + var sb strings.Builder + + if mc.Message != "" { + sb.WriteString(mc.Message) + } + + currentConflicts := len(mc.CurrentTemplateWarnings) != 0 || mc.CurrentTemplateError != nil + updateConflicts := len(mc.LatestTemplateWarnings) != 0 || mc.LatestTemplateError != nil + + if !currentConflicts && !updateConflicts { + sb.WriteString("No workspace conflicts\n") + return sb.String() + } + + if currentConflicts { + if len(mc.CurrentTemplateWarnings) != 0 { + fmt.Fprintf(&sb, "Warnings: \n%s\n", strings.Join(mc.CurrentTemplateWarnings, "\n")) + } + if mc.CurrentTemplateError != nil { + fmt.Fprintf(&sb, "Errors: \n%s\n", strings.Join(mc.CurrentTemplateError.Msgs, "\n")) + } + } + + if !mc.CurrentTemplateIsLatest && updateConflicts { + sb.WriteString("If workspace is updated to the latest template:\n") + if len(mc.LatestTemplateWarnings) != 0 { + fmt.Fprintf(&sb, "Warnings: \n%s\n", strings.Join(mc.LatestTemplateWarnings, "\n")) + } + if mc.LatestTemplateError != nil { + fmt.Fprintf(&sb, "Errors: \n%s\n", strings.Join(mc.LatestTemplateError.Msgs, "\n")) + } + } + + return sb.String() +} + +type WorkspaceTemplateMergeConflicts []*WorkspaceTemplateMergeConflict + +func (mcs WorkspaceTemplateMergeConflicts) Summary() string { + var ( + sb strings.Builder + currentWarnings int + updateWarnings int + currentErrors int + updateErrors int + ) + + for _, mc := range mcs { + if len(mc.CurrentTemplateWarnings) != 0 { + currentWarnings++ + } + if len(mc.LatestTemplateWarnings) != 0 { + updateWarnings++ + } + if mc.CurrentTemplateError != nil { + currentErrors++ + } + if mc.LatestTemplateError != nil { + updateErrors++ + } + } + + if currentErrors == 0 && updateErrors == 0 && currentWarnings == 0 && updateWarnings == 0 { + sb.WriteString("No workspace conflicts\n") + return sb.String() + } + + if currentErrors != 0 { + fmt.Fprintf(&sb, "%d workspaces will not be able to be rebuilt\n", currentErrors) + } + if updateErrors != 0 { + fmt.Fprintf(&sb, "%d workspaces will not be able to be rebuilt if updated to the latest version\n", updateErrors) + } + if currentWarnings != 0 { + fmt.Fprintf(&sb, "%d workspaces will be impacted\n", currentWarnings) + } + if updateWarnings != 0 { + fmt.Fprintf(&sb, "%d workspaces will be impacted if updated to the latest version\n", updateWarnings) + } + + return sb.String() +} + +type TplError struct { + // Msgs are the human facing strings to present to the user. Since there can be multiple + // problems with a template, there might be multiple strings + Msgs []string `json:"messages"` +} + +func (c *DefaultClient) SetPolicyTemplate(ctx context.Context, templateID string, templateScope TemplateScope, dryRun bool) (*SetPolicyTemplateResponse, error) { + var ( + resp SetPolicyTemplateResponse + query = url.Values{} + ) + + req := SetPolicyTemplateRequest{ + TemplateID: templateID, + Type: string(templateScope), + } + + if dryRun { + query.Set("dry-run", "true") + } + + if err := c.requestBody(ctx, http.MethodPost, "/api/private/workspaces/template/policy", req, &resp, withQueryParams(query)); err != nil { + return nil, err + } + + return &resp, nil +} diff --git a/docs/coder_workspaces.md b/docs/coder_workspaces.md index 4402d92c..a7ec4615 100644 --- a/docs/coder_workspaces.md +++ b/docs/coder_workspaces.md @@ -26,6 +26,7 @@ Perform operations on the Coder workspaces owned by the active user. * [coder workspaces edit](coder_workspaces_edit.md) - edit an existing workspace and initiate a rebuild. * [coder workspaces edit-from-config](coder_workspaces_edit-from-config.md) - change the template a workspace is tracking * [coder workspaces ls](coder_workspaces_ls.md) - list all workspaces owned by the active user +* [coder workspaces policy-template](coder_workspaces_policy-template.md) - Set workspace policy template * [coder workspaces rebuild](coder_workspaces_rebuild.md) - rebuild a Coder workspace * [coder workspaces rm](coder_workspaces_rm.md) - remove Coder workspaces by name * [coder workspaces stop](coder_workspaces_stop.md) - stop Coder workspaces by name diff --git a/docs/coder_workspaces_policy-template.md b/docs/coder_workspaces_policy-template.md new file mode 100644 index 00000000..36bf34fc --- /dev/null +++ b/docs/coder_workspaces_policy-template.md @@ -0,0 +1,32 @@ +## coder workspaces policy-template + +Set workspace policy template + +### Synopsis + +Set workspace policy template or restore to default configuration. This feature is for site admins only. + +``` +coder workspaces policy-template [flags] +``` + +### Options + +``` + --default Restore policy template to default configuration + --dry-run skip setting policy template, but view errors/warnings about how this policy template would impact existing workspaces + -f, --filepath string full path to local policy template file. + -h, --help help for policy-template + --scope string scope of impact for the policy template. Supported values: site (default "site") +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces + diff --git a/internal/cmd/workspaces.go b/internal/cmd/workspaces.go index b8017baa..d1135cf4 100644 --- a/internal/cmd/workspaces.go +++ b/internal/cmd/workspaces.go @@ -47,6 +47,7 @@ func workspacesCmd() *cobra.Command { workspaceFromConfigCmd(true), workspaceFromConfigCmd(false), editWorkspaceCmd(), + setPolicyTemplate(), ) return cmd } @@ -752,3 +753,85 @@ func buildUpdateReq(ctx context.Context, client coder.Client, conf updateConf) ( } return &updateReq, nil } + +func setPolicyTemplate() *cobra.Command { + var ( + ref string + repo string + filepath string + dryRun bool + defaultTemplate bool + scope string + ) + + cmd := &cobra.Command{ + Use: "policy-template", + Short: "Set workspace policy template", + Long: "Set workspace policy template or restore to default configuration. This feature is for site admins only.", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + client, err := newClient(ctx, true) + if err != nil { + return err + } + + if scope != coder.TemplateScopeSite { + return clog.Error("Invalid 'scope' value", "Valid scope values: site") + } + + if filepath == "" && !defaultTemplate { + return clog.Error("Missing required parameter --filepath or --default", "Must specify a template to set") + } + + templateID := "" + if filepath != "" { + var rd io.Reader + b, err := ioutil.ReadFile(filepath) + if err != nil { + return xerrors.Errorf("read local file: %w", err) + } + rd = bytes.NewReader(b) + + req := coder.ParseTemplateRequest{ + RepoURL: repo, + Ref: ref, + Local: rd, + OrgID: coder.SkipTemplateOrg, + Filepath: ".coder/coder.yaml", + } + + version, err := client.ParseTemplate(ctx, req) + if err != nil { + return handleAPIError(err) + } + templateID = version.TemplateID + } + + resp, err := client.SetPolicyTemplate(ctx, templateID, coder.TemplateScope(scope), dryRun) + if err != nil { + return handleAPIError(err) + } + + for _, mc := range resp.MergeConflicts { + workspace, err := client.WorkspaceByID(ctx, mc.WorkspaceID) + if err != nil { + fmt.Printf("Workspace %q:\n", mc.WorkspaceID) + } else { + fmt.Printf("Workspace %q in organization %q:\n", workspace.Name, workspace.OrganizationID) + } + + fmt.Println(mc.String()) + } + + fmt.Println("Summary:") + fmt.Println(coder.WorkspaceTemplateMergeConflicts(resp.MergeConflicts).Summary()) + + return nil + }, + } + cmd.Flags().BoolVarP(&dryRun, "dry-run", "", false, "skip setting policy template, but view errors/warnings about how this policy template would impact existing workspaces") + cmd.Flags().StringVarP(&filepath, "filepath", "f", "", "full path to local policy template file.") + cmd.Flags().StringVar(&scope, "scope", "site", "scope of impact for the policy template. Supported values: site") + cmd.Flags().BoolVar(&defaultTemplate, "default", false, "Restore policy template to default configuration") + return cmd +} From 1ed07d2c4b69738e60d41ba092dd826470aaccdd Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 22 Jun 2021 12:25:01 -0500 Subject: [PATCH 050/128] [ch-13671] stop using global enable_netv2 in favor of workspace provider config (#370) * stop using global enable_netv2 in favor of workspace provider config * gendocs --- coder-sdk/workspace_providers.go | 1 + docs/coder_config-ssh.md | 1 - internal/cmd/configssh.go | 29 +++++------------------------ 3 files changed, 6 insertions(+), 25 deletions(-) diff --git a/coder-sdk/workspace_providers.go b/coder-sdk/workspace_providers.go index 1ed0589c..5270389a 100644 --- a/coder-sdk/workspace_providers.go +++ b/coder-sdk/workspace_providers.go @@ -19,6 +19,7 @@ type KubernetesProvider struct { EnvproxyAccessURL string `json:"envproxy_access_url" table:"Access URL" validate:"required"` DevurlHost string `json:"devurl_host" table:"Devurl Host"` OrgWhitelist []string `json:"org_whitelist" table:"-"` + EnableNetV2 bool `json:"enable_net_v2" table:"Enable NetV2"` KubeProviderConfig `json:"config" table:"_"` } diff --git a/docs/coder_config-ssh.md b/docs/coder_config-ssh.md index c16d087b..1816b29a 100644 --- a/docs/coder_config-ssh.md +++ b/docs/coder_config-ssh.md @@ -15,7 +15,6 @@ coder config-ssh [flags] ``` --filepath string override the default path of your ssh config file (default "~/.ssh/config") -h, --help help for config-ssh - --next (alpha) uses coder tunnel to proxy ssh connection --remove remove the auto-generated Coder ssh config ``` diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index a482bbe7..bb0b39c9 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -35,23 +35,21 @@ func configSSHCmd() *cobra.Command { var ( configpath string remove = false - next = false ) cmd := &cobra.Command{ Use: "config-ssh", Short: "Configure SSH to access Coder workspaces", Long: "Inject the proper OpenSSH configuration into your local SSH config file.", - RunE: configSSH(&configpath, &remove, &next), + RunE: configSSH(&configpath, &remove), } cmd.Flags().StringVar(&configpath, "filepath", filepath.Join("~", ".ssh", "config"), "override the default path of your ssh config file") cmd.Flags().BoolVar(&remove, "remove", false, "remove the auto-generated Coder ssh config") - cmd.Flags().BoolVar(&next, "next", false, "(alpha) uses coder tunnel to proxy ssh connection") return cmd } -func configSSH(configpath *string, remove *bool, next *bool) func(cmd *cobra.Command, _ []string) error { +func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []string) error { return func(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() usr, err := user.Current() @@ -115,29 +113,12 @@ func configSSH(configpath *string, remove *bool, next *bool) func(cmd *cobra.Com return xerrors.New("SSH is disabled or not available for any workspaces in your Coder deployment.") } - wconf, err := client.SiteConfigWorkspaces(ctx) - if err != nil { - return xerrors.Errorf("getting site workspace config: %w", err) - } - p2p := false - if wconf.EnableP2P { - if *next { - p2p = true - } else { - fmt.Println("Note: NetworkingV2 is enabled on the coder deployment, use --next to enable it for ssh") - } - } else { - if *next { - return xerrors.New("NetworkingV2 feature is not enabled, cannot use --next flag") - } - } - binPath, err := os.Executable() if err != nil { return xerrors.Errorf("Failed to get executable path: %w", err) } - newConfig := makeNewConfigs(binPath, user.Username, workspacesWithProviders, privateKeyFilepath, p2p) + newConfig := makeNewConfigs(binPath, user.Username, workspacesWithProviders, privateKeyFilepath) err = os.MkdirAll(filepath.Dir(*configpath), os.ModePerm) if err != nil { @@ -198,7 +179,7 @@ func writeSSHKey(ctx context.Context, client coder.Client, privateKeyPath string return ioutil.WriteFile(privateKeyPath, []byte(key.PrivateKey), 0600) } -func makeNewConfigs(binPath, userName string, workspaces []coderutil.WorkspaceWithWorkspaceProvider, privateKeyFilepath string, p2p bool) string { +func makeNewConfigs(binPath, userName string, workspaces []coderutil.WorkspaceWithWorkspaceProvider, privateKeyFilepath string) string { newConfig := fmt.Sprintf("\n%s\n%s\n\n", sshStartToken, sshStartMessage) sort.Slice(workspaces, func(i, j int) bool { return workspaces[i].Workspace.Name < workspaces[j].Workspace.Name }) @@ -217,7 +198,7 @@ func makeNewConfigs(binPath, userName string, workspaces []coderutil.WorkspaceWi continue } - useTunnel := workspace.WorkspaceProvider.BuiltIn && p2p + useTunnel := workspace.WorkspaceProvider.SSHEnabled && workspace.WorkspaceProvider.EnableNetV2 newConfig += makeSSHConfig(binPath, u.Host, userName, workspace.Workspace.Name, privateKeyFilepath, useTunnel) } newConfig += fmt.Sprintf("\n%s\n", sshEndToken) From 14062a18b085332c224de23c87ad387fffbe1ca1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jun 2021 15:20:39 -0700 Subject: [PATCH 051/128] chore: bump github.com/briandowns/spinner from 1.15.0 to 1.16.0 (#371) Bumps [github.com/briandowns/spinner](https://github.com/briandowns/spinner) from 1.15.0 to 1.16.0. - [Release notes](https://github.com/briandowns/spinner/releases) - [Commits](https://github.com/briandowns/spinner/compare/v1.15.0...v1.16.0) --- updated-dependencies: - dependency-name: github.com/briandowns/spinner dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 14c140be..c5639b21 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.14 require ( cdr.dev/slog v1.4.1 cdr.dev/wsep v0.0.0-20200728013649-82316a09813f - github.com/briandowns/spinner v1.15.0 + github.com/briandowns/spinner v1.16.0 github.com/fatih/color v1.12.0 github.com/google/go-cmp v0.5.6 github.com/gorilla/websocket v1.4.2 diff --git a/go.sum b/go.sum index 4ad25ea8..d0455f92 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/briandowns/spinner v1.15.0 h1:L0jR0MYN7OAeMwpTzDZWIeqyDLXtTeJFxqoq+sL0VQM= -github.com/briandowns/spinner v1.15.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= +github.com/briandowns/spinner v1.16.0 h1:DFmp6hEaIx2QXXuqSJmtfSBSAjRmpGiKG6ip2Wm/yOs= +github.com/briandowns/spinner v1.16.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= From 196a2552f5824194ca034ff417bf4976803445c9 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 1 Jul 2021 13:54:55 -0500 Subject: [PATCH 052/128] fix: Return error on pinging closed connections (#373) --- wsnet/dial.go | 3 +++ wsnet/dial_test.go | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/wsnet/dial.go b/wsnet/dial.go index 9debf47a..eaab8938 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -176,6 +176,9 @@ func (d *Dialer) Close() error { // Ping sends a ping through the control channel. func (d *Dialer) Ping(ctx context.Context) error { + if d.ctrl.ReadyState() == webrtc.DataChannelStateClosed || d.ctrl.ReadyState() == webrtc.DataChannelStateClosing { + return webrtc.ErrConnectionClosed + } // Since we control the client and server we could open this // data channel with `Negotiated` true to reduce traffic being // sent when the RTC connection is opened. diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 50bdd938..3e52f17b 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -161,6 +161,29 @@ func TestDial(t *testing.T) { return } }) + + t.Run("Disconnect", func(t *testing.T) { + connectAddr, listenAddr := createDumbBroker(t) + _, err := Listen(context.Background(), listenAddr) + if err != nil { + t.Error(err) + return + } + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + if err != nil { + t.Error(err) + return + } + err = dialer.Close() + if err != nil { + t.Error(err) + return + } + err = dialer.Ping(context.Background()) + if err != webrtc.ErrConnectionClosed { + t.Error(err) + } + }) } func BenchmarkThroughput(b *testing.B) { From 4188f118bad3896d10f9ae05b397d90425f994a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jul 2021 10:29:01 -0700 Subject: [PATCH 053/128] chore: bump github.com/spf13/cobra from 1.1.3 to 1.2.1 (#376) Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.1.3 to 1.2.1. - [Release notes](https://github.com/spf13/cobra/releases) - [Changelog](https://github.com/spf13/cobra/blob/master/CHANGELOG.md) - [Commits](https://github.com/spf13/cobra/compare/v1.1.3...v1.2.1) --- updated-dependencies: - dependency-name: github.com/spf13/cobra dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 3 +- go.sum | 114 ++++++++++++++++++++------------------------------------- 2 files changed, 41 insertions(+), 76 deletions(-) diff --git a/go.mod b/go.mod index c5639b21..e9532be3 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f github.com/klauspost/compress v1.10.8 // indirect github.com/manifoldco/promptui v0.8.0 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/pion/datachannel v1.4.21 github.com/pion/dtls/v2 v2.0.9 github.com/pion/ice/v2 v2.1.7 @@ -21,7 +22,7 @@ require ( github.com/pion/webrtc/v3 v3.0.29 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 - github.com/spf13/cobra v1.1.3 + github.com/spf13/cobra v1.2.1 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 diff --git a/go.sum b/go.sum index d0455f92..6e5ed76c 100644 --- a/go.sum +++ b/go.sum @@ -49,7 +49,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= @@ -64,19 +63,15 @@ github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/briandowns/spinner v1.16.0 h1:DFmp6hEaIx2QXXuqSJmtfSBSAjRmpGiKG6ip2Wm/yOs= github.com/briandowns/spinner v1.16.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= @@ -87,11 +82,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= @@ -102,8 +94,6 @@ github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= @@ -128,9 +118,6 @@ github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= @@ -139,7 +126,6 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= @@ -147,10 +133,9 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -230,9 +215,7 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -261,25 +244,22 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU= github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.10.8 h1:eLeJ3dr/Y9+XRfJT4l+8ZjmtB5RPJhucH2HeCV5+IZY= github.com/klauspost/compress v1.10.8/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -289,7 +269,7 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo= github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -304,27 +284,24 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= @@ -332,7 +309,7 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0= github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= github.com/pion/dtls/v2 v2.0.9 h1:7Ow+V++YSZQMYzggI0P9vLJz/hUFcffsfGMfT/Qy+u8= @@ -377,22 +354,14 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8= github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -403,22 +372,18 @@ github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -427,14 +392,12 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -443,7 +406,9 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 go.coder.com/cli v0.4.0/go.mod h1:hRTOURCR3LJF1FRW9arecgrzX+AHG7mfYMwThPIgq+w= go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512 h1:DjCS6dRQh+1PlfiBmnabxfdrzenb0tAwJqFxDEH/s9g= go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512/go.mod h1:83JsYgXYv0EOaXjIMnaZ1Fl6ddNB3fJnDZ/8845mUJ8= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -452,14 +417,14 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -504,9 +469,7 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -557,6 +520,7 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -572,12 +536,9 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cO golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -624,6 +585,7 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -644,7 +606,6 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -684,6 +645,7 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -693,6 +655,7 @@ golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -722,6 +685,7 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -753,6 +717,7 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= @@ -785,6 +750,7 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= @@ -808,26 +774,24 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 4e5bbd73dea21c1de9988e7aed44a8fcc3d57bcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jul 2021 10:44:20 -0700 Subject: [PATCH 054/128] chore: bump github.com/pion/ice/v2 from 2.1.7 to 2.1.8 (#377) Bumps [github.com/pion/ice/v2](https://github.com/pion/ice) from 2.1.7 to 2.1.8. - [Release notes](https://github.com/pion/ice/releases) - [Commits](https://github.com/pion/ice/compare/v2.1.7...v2.1.8) --- updated-dependencies: - dependency-name: github.com/pion/ice/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index e9532be3..9c628c7d 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/pion/datachannel v1.4.21 github.com/pion/dtls/v2 v2.0.9 - github.com/pion/ice/v2 v2.1.7 + github.com/pion/ice/v2 v2.1.8 github.com/pion/logging v0.2.2 github.com/pion/turn/v2 v2.0.5 github.com/pion/webrtc/v3 v3.0.29 diff --git a/go.sum b/go.sum index 6e5ed76c..39d8a662 100644 --- a/go.sum +++ b/go.sum @@ -314,8 +314,9 @@ github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXm github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= github.com/pion/dtls/v2 v2.0.9 h1:7Ow+V++YSZQMYzggI0P9vLJz/hUFcffsfGMfT/Qy+u8= github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQKYQho= -github.com/pion/ice/v2 v2.1.7 h1:FjgDfUNrVYTxQabJrkBX6ld12tvYbgzHenqPh3PJF6E= github.com/pion/ice/v2 v2.1.7/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= +github.com/pion/ice/v2 v2.1.8 h1:3kV4XaB2C3z1gDUXZmwSB/B0PSdZ7GFFC3w4iUX9prs= +github.com/pion/ice/v2 v2.1.8/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= github.com/pion/interceptor v0.0.12 h1:eC1iVneBIAQJEfaNAfDqAncJWhMDAnaXPRCJsltdokE= github.com/pion/interceptor v0.0.12/go.mod h1:qzeuWuD/ZXvPqOnxNcnhWfkCZ2e1kwwslicyyPnhoK4= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= From e13b801a211453872ebe2c180c9a46807f4bf29a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jul 2021 11:02:49 -0700 Subject: [PATCH 055/128] chore: bump github.com/pion/webrtc/v3 from 3.0.29 to 3.0.30 (#372) Bumps [github.com/pion/webrtc/v3](https://github.com/pion/webrtc) from 3.0.29 to 3.0.30. - [Release notes](https://github.com/pion/webrtc/releases) - [Commits](https://github.com/pion/webrtc/compare/v3.0.29...v3.0.30) --- updated-dependencies: - dependency-name: github.com/pion/webrtc/v3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 9c628c7d..97020433 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/pion/ice/v2 v2.1.8 github.com/pion/logging v0.2.2 github.com/pion/turn/v2 v2.0.5 - github.com/pion/webrtc/v3 v3.0.29 + github.com/pion/webrtc/v3 v3.0.30 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.2.1 diff --git a/go.sum b/go.sum index 39d8a662..ec6a97fa 100644 --- a/go.sum +++ b/go.sum @@ -317,8 +317,8 @@ github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQK github.com/pion/ice/v2 v2.1.7/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= github.com/pion/ice/v2 v2.1.8 h1:3kV4XaB2C3z1gDUXZmwSB/B0PSdZ7GFFC3w4iUX9prs= github.com/pion/ice/v2 v2.1.8/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= -github.com/pion/interceptor v0.0.12 h1:eC1iVneBIAQJEfaNAfDqAncJWhMDAnaXPRCJsltdokE= -github.com/pion/interceptor v0.0.12/go.mod h1:qzeuWuD/ZXvPqOnxNcnhWfkCZ2e1kwwslicyyPnhoK4= +github.com/pion/interceptor v0.0.13 h1:fnV+b0p/KEzwwr/9z2nsSqA9IQRMsM4nF5HjrNSWwBo= +github.com/pion/interceptor v0.0.13/go.mod h1:svsW2QoLHLoGLUr4pDoSopGBEWk8FZwlfxId/OKRKzo= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw= @@ -347,8 +347,8 @@ github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= -github.com/pion/webrtc/v3 v3.0.29 h1:pVs6mYjbbYvC8pMsztayEz35DnUEFLPswsicGXaQjxo= -github.com/pion/webrtc/v3 v3.0.29/go.mod h1:XFQeLYBf++bWWA0sJqh6zF1ouWluosxwTOMOoTZGaD0= +github.com/pion/webrtc/v3 v3.0.30 h1:Q+U0mdIMwaeVZ846nkgHV5VRmDsrMtRvogVwBsXTFq4= +github.com/pion/webrtc/v3 v3.0.30/go.mod h1:IN0o0uQMUbPvcPZ8t1nQfg6tkUeJvsII8PsQIDW61jk= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -507,9 +507,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210420210106-798c2154c571/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -587,7 +587,6 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= From 732b0bfd609dbaba2c7e6955152893897caab1e9 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 7 Jul 2021 16:01:25 -0500 Subject: [PATCH 056/128] chore: Cache SSH connections (#378) * chore: Cache SSH connections * Fix listen disconnect --- internal/cmd/configssh.go | 12 +++++++++++- wsnet/listen.go | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index bb0b39c9..f4598837 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -8,6 +8,7 @@ import ( "os" "os/user" "path/filepath" + "runtime" "sort" "strings" @@ -208,7 +209,7 @@ func makeNewConfigs(binPath, userName string, workspaces []coderutil.WorkspaceWi func makeSSHConfig(binPath, host, userName, workspaceName, privateKeyFilepath string, tunnel bool) string { if tunnel { - return fmt.Sprintf( + host := fmt.Sprintf( `Host coder.%s HostName coder.%s ProxyCommand %s tunnel %s 12213 stdio @@ -217,6 +218,15 @@ func makeSSHConfig(binPath, host, userName, workspaceName, privateKeyFilepath st IdentitiesOnly yes IdentityFile="%s" `, workspaceName, workspaceName, binPath, workspaceName, privateKeyFilepath) + + if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { + host += ` ControlMaster auto + ControlPath ~/.ssh/.connection-%r@%h:%p + ControlPersist 600 +` + } + + return host } return fmt.Sprintf( diff --git a/wsnet/listen.go b/wsnet/listen.go index 3a6735f0..3c7c3b3e 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -332,10 +332,10 @@ func (l *listener) handle(msg BrokerMessage) func(dc *webrtc.DataChannel) { rw: rw, } co.init() - defer co.Close() defer nc.Close() - + defer co.Close() go func() { + defer dc.Close() _, _ = io.Copy(co, nc) }() _, _ = io.Copy(nc, co) From 8be51b666ab7be5aa926df9d089eae15b42fda7b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Jul 2021 09:05:27 -0500 Subject: [PATCH 057/128] chore: bump github.com/pion/webrtc/v3 from 3.0.30 to 3.0.31 (#380) Bumps [github.com/pion/webrtc/v3](https://github.com/pion/webrtc) from 3.0.30 to 3.0.31. - [Release notes](https://github.com/pion/webrtc/releases) - [Commits](https://github.com/pion/webrtc/compare/v3.0.30...v3.0.31) --- updated-dependencies: - dependency-name: github.com/pion/webrtc/v3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 97020433..960091bb 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/pion/ice/v2 v2.1.8 github.com/pion/logging v0.2.2 github.com/pion/turn/v2 v2.0.5 - github.com/pion/webrtc/v3 v3.0.30 + github.com/pion/webrtc/v3 v3.0.31 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.2.1 diff --git a/go.sum b/go.sum index ec6a97fa..00a35ce5 100644 --- a/go.sum +++ b/go.sum @@ -314,7 +314,6 @@ github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXm github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= github.com/pion/dtls/v2 v2.0.9 h1:7Ow+V++YSZQMYzggI0P9vLJz/hUFcffsfGMfT/Qy+u8= github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQKYQho= -github.com/pion/ice/v2 v2.1.7/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= github.com/pion/ice/v2 v2.1.8 h1:3kV4XaB2C3z1gDUXZmwSB/B0PSdZ7GFFC3w4iUX9prs= github.com/pion/ice/v2 v2.1.8/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= github.com/pion/interceptor v0.0.13 h1:fnV+b0p/KEzwwr/9z2nsSqA9IQRMsM4nF5HjrNSWwBo= @@ -347,8 +346,8 @@ github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= -github.com/pion/webrtc/v3 v3.0.30 h1:Q+U0mdIMwaeVZ846nkgHV5VRmDsrMtRvogVwBsXTFq4= -github.com/pion/webrtc/v3 v3.0.30/go.mod h1:IN0o0uQMUbPvcPZ8t1nQfg6tkUeJvsII8PsQIDW61jk= +github.com/pion/webrtc/v3 v3.0.31 h1:nKvgtsOpft7+QU7eObCFYwnRAtG/HHSmeXg1VljYs5M= +github.com/pion/webrtc/v3 v3.0.31/go.mod h1:aaGJVQNQ/e9lSXcOR3jEDHnMdG6Pg3HkUMd2tv2fvYM= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= From 0d2f06b9f47c9582307dc359e3ccbabe6dad9dbd Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 8 Jul 2021 09:07:52 -0500 Subject: [PATCH 058/128] fix: Remove active connections when RTC connection is lost (#379) * fix: Remove active connections when RTC connection is lost * Move close injection * Fix linting * Fix linting --- wsnet/dial.go | 36 ++++++++++++++++++++++++++++-------- wsnet/dial_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ wsnet/rtc.go | 10 +++++++++- wsnet/rtc_test.go | 10 +++++----- wsnet/wsnet_test.go | 16 +++++++++++----- 5 files changed, 98 insertions(+), 19 deletions(-) diff --git a/wsnet/dial.go b/wsnet/dial.go index eaab8938..637bc5fd 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net" + "sync" "time" "github.com/pion/datachannel" @@ -81,9 +82,10 @@ func Dial(conn net.Conn, iceServers []webrtc.ICEServer) (*Dialer, error) { flushCandidates() dialer := &Dialer{ - conn: conn, - ctrl: ctrl, - rtc: rtc, + conn: conn, + ctrl: ctrl, + rtc: rtc, + connClosers: make([]io.Closer, 0), } return dialer, dialer.negotiate() @@ -97,6 +99,9 @@ type Dialer struct { ctrl *webrtc.DataChannel ctrlrw datachannel.ReadWriteCloser rtc *webrtc.PeerConnection + + connClosers []io.Closer + connClosersMut sync.Mutex } func (d *Dialer) negotiate() (err error) { @@ -111,16 +116,27 @@ func (d *Dialer) negotiate() (err error) { go func() { defer close(errCh) + defer func() { + _ = d.conn.Close() + }() err := waitForConnectionOpen(context.Background(), d.rtc) if err != nil { - _ = d.conn.Close() errCh <- err return } - go func() { - // Closing this connection took 30ms+. - _ = d.conn.Close() - }() + d.rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { + if pcs == webrtc.PeerConnectionStateConnected { + return + } + + // Close connections opened while the RTC was alive. + d.connClosersMut.Lock() + defer d.connClosersMut.Unlock() + for _, connCloser := range d.connClosers { + _ = connCloser.Close() + } + d.connClosers = make([]io.Closer, 0) + }) }() for { @@ -210,6 +226,10 @@ func (d *Dialer) DialContext(ctx context.Context, network, address string) (net. if err != nil { return nil, fmt.Errorf("create data channel: %w", err) } + d.connClosersMut.Lock() + d.connClosers = append(d.connClosers, dc) + d.connClosersMut.Unlock() + err = waitForDataChannelOpen(ctx, dc) if err != nil { return nil, fmt.Errorf("wait for open: %w", err) diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 3e52f17b..71fdc8c7 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -5,11 +5,13 @@ import ( "context" "crypto/rand" "errors" + "fmt" "io" "net" "strconv" "testing" + "github.com/pion/ice/v2" "github.com/pion/webrtc/v3" ) @@ -44,6 +46,7 @@ func ExampleDial_basic() { // You now have access to the proxied remote port in `conn`. } +// nolint:gocognit func TestDial(t *testing.T) { t.Run("Ping", func(t *testing.T) { connectAddr, listenAddr := createDumbBroker(t) @@ -184,6 +187,48 @@ func TestDial(t *testing.T) { t.Error(err) } }) + + t.Run("Disconnect DialContext", func(t *testing.T) { + tcpListener, err := net.Listen("tcp", "0.0.0.0:0") + if err != nil { + t.Error(err) + return + } + go func() { + _, _ = tcpListener.Accept() + }() + + connectAddr, listenAddr := createDumbBroker(t) + _, err = Listen(context.Background(), listenAddr) + if err != nil { + t.Error(err) + return + } + turnAddr, closeTurn := createTURNServer(t, ice.SchemeTypeTURN) + dialer, err := DialWebsocket(context.Background(), connectAddr, []webrtc.ICEServer{{ + URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, + Username: "example", + Credential: testPass, + CredentialType: webrtc.ICECredentialTypePassword, + }}) + if err != nil { + t.Error(err) + return + } + conn, err := dialer.DialContext(context.Background(), "tcp", tcpListener.Addr().String()) + if err != nil { + t.Error(err) + return + } + // Close the TURN server before reading... + // WebRTC connections take a few seconds to timeout. + closeTurn() + _, err = conn.Read(make([]byte, 16)) + if err != io.EOF { + t.Error(err) + return + } + }) } func BenchmarkThroughput(b *testing.B) { diff --git a/wsnet/rtc.go b/wsnet/rtc.go index 0605fa29..4d454311 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -164,6 +164,8 @@ func newPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, erro lf.DefaultLogLevel = logging.LogLevelDisabled se.LoggerFactory = lf + transportPolicy := webrtc.ICETransportPolicyAll + // If one server is provided and we know it's TURN, we can set the // relay acceptable so the connection starts immediately. if len(servers) == 1 { @@ -174,12 +176,18 @@ func newPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, erro se.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6}) se.SetRelayAcceptanceMinWait(0) } + if err == nil && (url.Scheme == ice.SchemeTypeTURN || url.Scheme == ice.SchemeTypeTURNS) { + // Local peers will connect if they discover they live on the same host. + // For testing purposes, it's simpler if they cannot peer on the same host. + transportPolicy = webrtc.ICETransportPolicyRelay + } } } api := webrtc.NewAPI(webrtc.WithSettingEngine(se)) return api.NewPeerConnection(webrtc.Configuration{ - ICEServers: servers, + ICEServers: servers, + ICETransportPolicy: transportPolicy, }) } diff --git a/wsnet/rtc_test.go b/wsnet/rtc_test.go index 14bdd846..73d1af2f 100644 --- a/wsnet/rtc_test.go +++ b/wsnet/rtc_test.go @@ -16,11 +16,11 @@ func TestDialICE(t *testing.T) { t.Run("TURN with TLS", func(t *testing.T) { t.Parallel() - addr := createTURNServer(t, ice.SchemeTypeTURNS, "test") + addr, _ := createTURNServer(t, ice.SchemeTypeTURNS) err := DialICE(webrtc.ICEServer{ URLs: []string{fmt.Sprintf("turns:%s", addr)}, Username: "example", - Credential: "test", + Credential: testPass, CredentialType: webrtc.ICECredentialTypePassword, }, &DialICEOptions{ Timeout: time.Millisecond, @@ -34,11 +34,11 @@ func TestDialICE(t *testing.T) { t.Run("Protocol mismatch", func(t *testing.T) { t.Parallel() - addr := createTURNServer(t, ice.SchemeTypeTURNS, "test") + addr, _ := createTURNServer(t, ice.SchemeTypeTURNS) err := DialICE(webrtc.ICEServer{ URLs: []string{fmt.Sprintf("turn:%s", addr)}, Username: "example", - Credential: "test", + Credential: testPass, CredentialType: webrtc.ICECredentialTypePassword, }, &DialICEOptions{ Timeout: time.Millisecond, @@ -52,7 +52,7 @@ func TestDialICE(t *testing.T) { t.Run("Invalid auth", func(t *testing.T) { t.Parallel() - addr := createTURNServer(t, ice.SchemeTypeTURNS, "test") + addr, _ := createTURNServer(t, ice.SchemeTypeTURNS) err := DialICE(webrtc.ICEServer{ URLs: []string{fmt.Sprintf("turns:%s", addr)}, Username: "example", diff --git a/wsnet/wsnet_test.go b/wsnet/wsnet_test.go index fc14cd3c..ad9ac381 100644 --- a/wsnet/wsnet_test.go +++ b/wsnet/wsnet_test.go @@ -25,6 +25,11 @@ import ( "nhooyr.io/websocket" ) +const ( + // Password used connecting to the test TURN server. + testPass = "test" +) + // createDumbBroker proxies sockets between /listen and /connect // to emulate an authenticated WebSocket pair. func createDumbBroker(t testing.TB) (connectAddr string, listenAddr string) { @@ -86,7 +91,7 @@ func createDumbBroker(t testing.TB) (connectAddr string, listenAddr string) { } // createTURNServer allocates a TURN server and returns the address. -func createTURNServer(t *testing.T, server ice.SchemeType, pass string) string { +func createTURNServer(t *testing.T, server ice.SchemeType) (string, func()) { var ( listeners []turn.ListenerConfig pcListeners []turn.PacketConnConfig @@ -136,14 +141,14 @@ func createTURNServer(t *testing.T, server ice.SchemeType, pass string) string { ListenerConfigs: listeners, Realm: "coder", AuthHandler: func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) { - return turn.GenerateAuthKey(username, realm, pass), true + return turn.GenerateAuthKey(username, realm, testPass), true }, LoggerFactory: lf, }) if err != nil { t.Error(err) } - t.Cleanup(func() { + closeFunc := func() { for _, l := range listeners { l.Listener.Close() } @@ -151,9 +156,10 @@ func createTURNServer(t *testing.T, server ice.SchemeType, pass string) string { l.PacketConn.Close() } srv.Close() - }) + } + t.Cleanup(closeFunc) - return listenAddr.String() + return listenAddr.String(), closeFunc } func generateTLSConfig(t testing.TB) *tls.Config { From 7f3cd286eb7cb2cacd5acc32f440d8a3079760dc Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 8 Jul 2021 23:46:05 -0500 Subject: [PATCH 059/128] feat: Add Closed func to Dialer (#381) * feat: Add Closed func to Dialer * Fix gocyclo * Cleanup closed bool --- wsnet/dial.go | 15 +++++++++++++++ wsnet/dial_test.go | 25 ++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/wsnet/dial.go b/wsnet/dial.go index 637bc5fd..c72dc513 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -85,6 +85,7 @@ func Dial(conn net.Conn, iceServers []webrtc.ICEServer) (*Dialer, error) { conn: conn, ctrl: ctrl, rtc: rtc, + closedChan: make(chan struct{}), connClosers: make([]io.Closer, 0), } @@ -100,6 +101,7 @@ type Dialer struct { ctrlrw datachannel.ReadWriteCloser rtc *webrtc.PeerConnection + closedChan chan struct{} connClosers []io.Closer connClosersMut sync.Mutex } @@ -136,6 +138,13 @@ func (d *Dialer) negotiate() (err error) { _ = connCloser.Close() } d.connClosers = make([]io.Closer, 0) + + select { + case <-d.closedChan: + return + default: + } + close(d.closedChan) }) }() @@ -184,6 +193,12 @@ func (d *Dialer) negotiate() (err error) { return <-errCh } +// Closed returns a channel that closes when +// the connection is closed. +func (d *Dialer) Closed() <-chan struct{} { + return d.closedChan +} + // Close closes the RTC connection. // All data channels dialed will be closed. func (d *Dialer) Close() error { diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 71fdc8c7..9b412a3e 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -10,6 +10,7 @@ import ( "net" "strconv" "testing" + "time" "github.com/pion/ice/v2" "github.com/pion/webrtc/v3" @@ -46,7 +47,7 @@ func ExampleDial_basic() { // You now have access to the proxied remote port in `conn`. } -// nolint:gocognit +// nolint:gocognit,gocyclo func TestDial(t *testing.T) { t.Run("Ping", func(t *testing.T) { connectAddr, listenAddr := createDumbBroker(t) @@ -229,6 +230,28 @@ func TestDial(t *testing.T) { return } }) + + t.Run("Closed", func(t *testing.T) { + connectAddr, listenAddr := createDumbBroker(t) + _, err := Listen(context.Background(), listenAddr) + if err != nil { + t.Error(err) + return + } + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + if err != nil { + t.Error(err) + return + } + go func() { + _ = dialer.Close() + }() + select { + case <-dialer.Closed(): + case <-time.NewTimer(time.Second).C: + t.Error("didn't close in time") + } + }) } func BenchmarkThroughput(b *testing.B) { From 6cc203a192782d785f136bc1a541162b33fdb111 Mon Sep 17 00:00:00 2001 From: Jonathan Yu Date: Tue, 13 Jul 2021 18:32:30 -0700 Subject: [PATCH 060/128] chore: update dependabot version ignores (#383) --- .github/dependabot.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ebbc08af..f2976dc1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,9 +9,16 @@ updates: commit-message: prefix: "chore" ignore: - # GitHub always delivers the latest versions for each major - # release tag, so handle updates manually + # These actions deliver the latest versions by updating the major + # release tag, so ignore minor and patch versions - dependency-name: "actions/*" + update-types: + - "version-update:semver-minor" + - "version-update:semver-patch" + - dependency-name: "Apple-Actions/import-codesign-certs" + update-types: + - "version-update:semver-minor" + - "version-update:semver-patch" - package-ecosystem: "gomod" directory: "/" From a8443d00b29e50c64d9b10984f75c0b65529d9f9 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 13 Jul 2021 21:58:26 -0500 Subject: [PATCH 061/128] fix: Close Ping DataChannel when connection ends (#382) Previously, Ping() would hang forever due to the DataChannel never closing when the RTC connection ended. --- wsnet/dial.go | 2 +- wsnet/dial_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ wsnet/rtc.go | 5 ++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/wsnet/dial.go b/wsnet/dial.go index c72dc513..0beb2232 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -86,7 +86,7 @@ func Dial(conn net.Conn, iceServers []webrtc.ICEServer) (*Dialer, error) { ctrl: ctrl, rtc: rtc, closedChan: make(chan struct{}), - connClosers: make([]io.Closer, 0), + connClosers: []io.Closer{ctrl}, } return dialer, dialer.negotiate() diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 9b412a3e..5d2e3884 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -50,6 +50,8 @@ func ExampleDial_basic() { // nolint:gocognit,gocyclo func TestDial(t *testing.T) { t.Run("Ping", func(t *testing.T) { + t.Parallel() + connectAddr, listenAddr := createDumbBroker(t) _, err := Listen(context.Background(), listenAddr) if err != nil { @@ -67,7 +69,38 @@ func TestDial(t *testing.T) { } }) + t.Run("Ping Close", func(t *testing.T) { + t.Parallel() + + connectAddr, listenAddr := createDumbBroker(t) + _, err := Listen(context.Background(), listenAddr) + if err != nil { + t.Error(err) + return + } + turnAddr, closeTurn := createTURNServer(t, ice.SchemeTypeTURN) + dialer, err := DialWebsocket(context.Background(), connectAddr, []webrtc.ICEServer{{ + URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, + Username: "example", + Credential: testPass, + CredentialType: webrtc.ICECredentialTypePassword, + }}) + if err != nil { + t.Error(err) + return + } + _ = dialer.Ping(context.Background()) + closeTurn() + err = dialer.Ping(context.Background()) + if err != io.EOF { + t.Error(err) + return + } + }) + t.Run("OPError", func(t *testing.T) { + t.Parallel() + connectAddr, listenAddr := createDumbBroker(t) _, err := Listen(context.Background(), listenAddr) if err != nil { @@ -91,6 +124,8 @@ func TestDial(t *testing.T) { }) t.Run("Proxy", func(t *testing.T) { + t.Parallel() + listener, err := net.Listen("tcp", "0.0.0.0:0") if err != nil { t.Error(err) @@ -134,6 +169,8 @@ func TestDial(t *testing.T) { // Expect that we'd get an EOF on the server closing. t.Run("EOF on Close", func(t *testing.T) { + t.Parallel() + listener, err := net.Listen("tcp", "0.0.0.0:0") if err != nil { t.Error(err) @@ -167,6 +204,8 @@ func TestDial(t *testing.T) { }) t.Run("Disconnect", func(t *testing.T) { + t.Parallel() + connectAddr, listenAddr := createDumbBroker(t) _, err := Listen(context.Background(), listenAddr) if err != nil { @@ -190,6 +229,8 @@ func TestDial(t *testing.T) { }) t.Run("Disconnect DialContext", func(t *testing.T) { + t.Parallel() + tcpListener, err := net.Listen("tcp", "0.0.0.0:0") if err != nil { t.Error(err) @@ -232,6 +273,8 @@ func TestDial(t *testing.T) { }) t.Run("Closed", func(t *testing.T) { + t.Parallel() + connectAddr, listenAddr := createDumbBroker(t) _, err := Listen(context.Background(), listenAddr) if err != nil { diff --git a/wsnet/rtc.go b/wsnet/rtc.go index 4d454311..e8b5eab3 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -159,7 +159,7 @@ func newPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, erro se.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeUDP4}) se.SetSrflxAcceptanceMinWait(0) se.DetachDataChannels() - se.SetICETimeouts(time.Second*5, time.Second*5, time.Second*2) + se.SetICETimeouts(time.Second*3, time.Second*3, time.Second*2) lf := logging.NewDefaultLoggerFactory() lf.DefaultLogLevel = logging.LogLevelDisabled se.LoggerFactory = lf @@ -252,6 +252,9 @@ func waitForDataChannelOpen(ctx context.Context, channel *webrtc.DataChannel) er if channel.ReadyState() == webrtc.DataChannelStateOpen { return nil } + if channel.ReadyState() != webrtc.DataChannelStateConnecting { + return fmt.Errorf("channel closed") + } ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) defer cancelFunc() channel.OnOpen(func() { From 0e66c05df3f7c750978be8b493c13801cb2f5103 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 13 Jul 2021 23:25:23 -0500 Subject: [PATCH 062/128] feat: Enable TURN proxying over WebSocket (#384) * feat: Enable TURN proxying over WebSocket * Clean up API * Add net dependency * Close ws body * Add nop credentials to TURN candidate * Fix body close * Rename conn to dataChannelConn * Wrap websocket * Don't dial bad candidate * Refactor API * Fix deadline exceeding * Add comments * Try listing failed files * Organize imports * Fix test * Cleanup turnProxyConn impl --- ci/scripts/files_changed.sh | 1 + go.mod | 1 + internal/cmd/agent.go | 2 +- internal/cmd/tunnel.go | 32 +++-------- wsnet/auth.go | 22 -------- wsnet/conn.go | 109 +++++++++++++++++++++++++++++------- wsnet/dial.go | 37 +++++++++--- wsnet/dial_test.go | 50 +++++++++-------- wsnet/listen.go | 16 ++++-- wsnet/listen_test.go | 2 +- wsnet/rtc.go | 13 ++++- 11 files changed, 179 insertions(+), 106 deletions(-) delete mode 100644 wsnet/auth.go diff --git a/ci/scripts/files_changed.sh b/ci/scripts/files_changed.sh index 759c68d3..490cb5ad 100755 --- a/ci/scripts/files_changed.sh +++ b/ci/scripts/files_changed.sh @@ -6,6 +6,7 @@ cd "$(git rev-parse --show-toplevel)" if [[ $(git ls-files --other --modified --exclude-standard) ]]; then echo "Files have changed:" + git ls-files --other --modified --exclude-standard git -c color.ui=never status exit 1 fi diff --git a/go.mod b/go.mod index 960091bb..46a9d7ce 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.2.1 + golang.org/x/net v0.0.0-20210614182718-04defd469f4e golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go index 38853dd1..c19bdfee 100644 --- a/internal/cmd/agent.go +++ b/internal/cmd/agent.go @@ -73,7 +73,7 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx } } - listener, err := wsnet.Listen(context.Background(), wsnet.ListenEndpoint(u, token)) + listener, err := wsnet.Listen(context.Background(), wsnet.ListenEndpoint(u, token), wsnet.TURNProxyWebSocket(u, token)) if err != nil { return xerrors.Errorf("listen: %w", err) } diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index ae7fe14c..7b14cf33 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -2,7 +2,6 @@ package cmd import ( "context" - "errors" "fmt" "io" "net" @@ -12,7 +11,6 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" - "github.com/pion/webrtc/v3" "github.com/spf13/cobra" "golang.org/x/xerrors" @@ -104,30 +102,14 @@ type tunnneler struct { } func (c *tunnneler) start(ctx context.Context) error { - username, password, err := wsnet.TURNCredentials(c.token) - if err != nil { - return xerrors.Errorf("failed to parse credentials from token") - } - server := webrtc.ICEServer{ - URLs: []string{wsnet.TURNEndpoint(c.brokerAddr)}, - Username: username, - Credential: password, - CredentialType: webrtc.ICECredentialTypePassword, - } - - err = wsnet.DialICE(server, nil) - if errors.Is(err, wsnet.ErrInvalidCredentials) { - return xerrors.Errorf("failed to authenticate your user for this workspace") - } - if errors.Is(err, wsnet.ErrMismatchedProtocol) { - return xerrors.Errorf("your TURN server is configured incorrectly. check TLS settings") - } - if err != nil { - return xerrors.Errorf("dial ice: %w", err) - } - c.log.Debug(ctx, "Connecting to workspace...") - wd, err := wsnet.DialWebsocket(ctx, wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token), []webrtc.ICEServer{server}) + wd, err := wsnet.DialWebsocket( + ctx, + wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token), + &wsnet.DialOptions{ + TURNProxy: wsnet.TURNProxyWebSocket(c.brokerAddr, c.token), + }, + ) if err != nil { return xerrors.Errorf("creating workspace dialer: %w", err) } diff --git a/wsnet/auth.go b/wsnet/auth.go deleted file mode 100644 index a5daf45e..00000000 --- a/wsnet/auth.go +++ /dev/null @@ -1,22 +0,0 @@ -package wsnet - -import ( - "crypto/sha256" - "encoding/base64" - "errors" - "strings" -) - -// TURNCredentials returns a username and password pair -// for a Coder token. -func TURNCredentials(token string) (username, password string, err error) { - str := strings.SplitN(token, "-", 2) - if len(str) != 2 { - err = errors.New("invalid token format") - return - } - username = str[0] - hash := sha256.Sum256([]byte(str[1])) - password = base64.StdEncoding.EncodeToString(hash[:]) - return -} diff --git a/wsnet/conn.go b/wsnet/conn.go index b5dea0a5..608c5c70 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -1,14 +1,20 @@ package wsnet import ( + "context" "fmt" "net" + "net/http" "net/url" "sync" "time" "github.com/pion/datachannel" "github.com/pion/webrtc/v3" + "golang.org/x/net/proxy" + "nhooyr.io/websocket" + + "cdr.dev/coder-cli/coder-sdk" ) const ( @@ -22,16 +28,6 @@ const ( maxMessageLength = 32 * 1024 // 32 KB ) -// TURNEndpoint returns the TURN address for a Coder baseURL. -func TURNEndpoint(baseURL *url.URL) string { - turnScheme := "turns" - if baseURL.Scheme == httpScheme { - turnScheme = "turn" - } - - return fmt.Sprintf("%s:%s:5349?transport=tcp", turnScheme, baseURL.Hostname()) -} - // ListenEndpoint returns the Coder endpoint to listen for workspace connections. func ListenEndpoint(baseURL *url.URL, token string) string { wsScheme := "wss" @@ -50,7 +46,80 @@ func ConnectEndpoint(baseURL *url.URL, workspace, token string) string { return fmt.Sprintf("%s://%s%s%s%s%s", wsScheme, baseURL.Host, "/api/private/envagent/", workspace, "/connect?session_token=", token) } -type conn struct { +// TURNWebSocketICECandidate returns a valid relay ICEServer that can be used to +// trigger a TURNWebSocketDialer. +func TURNProxyICECandidate() webrtc.ICEServer { + return webrtc.ICEServer{ + URLs: []string{"turn:127.0.0.1:3478?transport=tcp"}, + Username: "~magicalusername~", + Credential: "~magicalpassword~", + CredentialType: webrtc.ICECredentialTypePassword, + } +} + +// TURNWebSocketDialer proxies all TURN traffic through a WebSocket. +func TURNProxyWebSocket(baseURL *url.URL, token string) proxy.Dialer { + return &turnProxyDialer{ + baseURL: baseURL, + token: token, + } +} + +// Proxies all TURN ICEServer traffic through this dialer. +// References Coder APIs with a specific token. +type turnProxyDialer struct { + baseURL *url.URL + token string +} + +func (t *turnProxyDialer) Dial(network, addr string) (c net.Conn, err error) { + headers := http.Header{} + headers.Set("Session-Token", t.token) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) + defer cancel() + + // Copy the baseURL so we can adjust path. + url := *t.baseURL + url.Scheme = "wss" + if url.Scheme == httpScheme { + url.Scheme = "ws" + } + url.Path = "/api/private/turn" + conn, resp, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{ + HTTPHeader: headers, + }) + if err != nil { + if resp != nil { + defer resp.Body.Close() + return nil, coder.NewHTTPError(resp) + } + return nil, fmt.Errorf("dial: %w", err) + } + + return &turnProxyConn{ + websocket.NetConn(context.Background(), conn, websocket.MessageBinary), + }, nil +} + +// turnProxyConn is a net.Conn wrapper that returns a TCPAddr for the +// LocalAddr function. pion/ice unsafely checks the types. See: +// https://github.com/pion/ice/blob/e78f26fb435987420546c70369ade5d713beca39/gather.go#L448 +type turnProxyConn struct { + net.Conn +} + +// The LocalAddr specified here doesn't really matter, +// it just has to be of type "TCPAddr". +func (*turnProxyConn) LocalAddr() net.Addr { + return &net.TCPAddr{ + IP: net.IPv4(127, 0, 0, 1), + Port: 0, + } +} + +// Properly buffers data for data channel connections. +type dataChannelConn struct { addr *net.UnixAddr dc *webrtc.DataChannel rw datachannel.ReadWriteCloser @@ -62,7 +131,7 @@ type conn struct { writeMutex sync.Mutex } -func (c *conn) init() { +func (c *dataChannelConn) init() { c.sendMore = make(chan struct{}, 1) c.dc.SetBufferedAmountLowThreshold(bufferedAmountLowThreshold) c.dc.OnBufferedAmountLow(func() { @@ -78,11 +147,11 @@ func (c *conn) init() { }) } -func (c *conn) Read(b []byte) (n int, err error) { +func (c *dataChannelConn) Read(b []byte) (n int, err error) { return c.rw.Read(b) } -func (c *conn) Write(b []byte) (n int, err error) { +func (c *dataChannelConn) Write(b []byte) (n int, err error) { c.writeMutex.Lock() defer c.writeMutex.Unlock() if len(b) > maxMessageLength { @@ -101,7 +170,7 @@ func (c *conn) Write(b []byte) (n int, err error) { return c.rw.Write(b) } -func (c *conn) Close() error { +func (c *dataChannelConn) Close() error { c.closedMutex.Lock() defer c.closedMutex.Unlock() if !c.closed { @@ -111,22 +180,22 @@ func (c *conn) Close() error { return c.dc.Close() } -func (c *conn) LocalAddr() net.Addr { +func (c *dataChannelConn) LocalAddr() net.Addr { return c.addr } -func (c *conn) RemoteAddr() net.Addr { +func (c *dataChannelConn) RemoteAddr() net.Addr { return c.addr } -func (c *conn) SetDeadline(t time.Time) error { +func (c *dataChannelConn) SetDeadline(t time.Time) error { return nil } -func (c *conn) SetReadDeadline(t time.Time) error { +func (c *dataChannelConn) SetReadDeadline(t time.Time) error { return nil } -func (c *conn) SetWriteDeadline(t time.Time) error { +func (c *dataChannelConn) SetWriteDeadline(t time.Time) error { return nil } diff --git a/wsnet/dial.go b/wsnet/dial.go index 0beb2232..362bbab9 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -12,13 +12,26 @@ import ( "github.com/pion/datachannel" "github.com/pion/webrtc/v3" + "golang.org/x/net/proxy" "nhooyr.io/websocket" "cdr.dev/coder-cli/coder-sdk" ) +// DialOptions are configurable options for a wsnet connection. +type DialOptions struct { + // ICEServers is an array of STUN or TURN servers to use for negotiation purposes. + // See: https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration/iceServers + ICEServers []webrtc.ICEServer + + // TURNProxy is a function used to proxy all TURN traffic. + // If specified without ICEServers, `TURNProxyICECandidate` + // will be used. + TURNProxy proxy.Dialer +} + // DialWebsocket dials the broker with a WebSocket and negotiates a connection. -func DialWebsocket(ctx context.Context, broker string, iceServers []webrtc.ICEServer) (*Dialer, error) { +func DialWebsocket(ctx context.Context, broker string, options *DialOptions) (*Dialer, error) { conn, resp, err := websocket.Dial(ctx, broker, nil) if err != nil { if resp != nil { @@ -35,16 +48,24 @@ func DialWebsocket(ctx context.Context, broker string, iceServers []webrtc.ICESe // We should close the socket intentionally. _ = conn.Close(websocket.StatusInternalError, "an error occurred") }() - return Dial(nconn, iceServers) + return Dial(nconn, options) } // Dial negotiates a connection to a listener. -func Dial(conn net.Conn, iceServers []webrtc.ICEServer) (*Dialer, error) { - if iceServers == nil { - iceServers = []webrtc.ICEServer{} +func Dial(conn net.Conn, options *DialOptions) (*Dialer, error) { + if options == nil { + options = &DialOptions{} + } + if options.ICEServers == nil { + options.ICEServers = []webrtc.ICEServer{} + } + // If the TURNProxy is specified and ICEServers aren't, + // it's safe to assume we can inject the default proxy candidate. + if len(options.ICEServers) == 0 && options.TURNProxy != nil { + options.ICEServers = []webrtc.ICEServer{TURNProxyICECandidate()} } - rtc, err := newPeerConnection(iceServers) + rtc, err := newPeerConnection(options.ICEServers, options.TURNProxy) if err != nil { return nil, fmt.Errorf("create peer connection: %w", err) } @@ -70,7 +91,7 @@ func Dial(conn net.Conn, iceServers []webrtc.ICEServer) (*Dialer, error) { offerMessage, err := json.Marshal(&BrokerMessage{ Offer: &offer, - Servers: iceServers, + Servers: options.ICEServers, }) if err != nil { return nil, fmt.Errorf("marshal offer message: %w", err) @@ -287,7 +308,7 @@ func (d *Dialer) DialContext(ctx context.Context, network, address string) (net. return nil, ctx.Err() } - c := &conn{ + c := &dataChannelConn{ addr: &net.UnixAddr{ Name: address, Net: network, diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 5d2e3884..6ad27866 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -35,7 +35,9 @@ func ExampleDial_basic() { } } - dialer, err := DialWebsocket(context.Background(), "wss://master.cdr.dev/agent/workspace/connect", servers) + dialer, err := DialWebsocket(context.Background(), "wss://master.cdr.dev/agent/workspace/connect", &DialOptions{ + ICEServers: servers, + }) if err != nil { // Do something... } @@ -53,7 +55,7 @@ func TestDial(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr) + _, err := Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return @@ -73,18 +75,20 @@ func TestDial(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr) + _, err := Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return } turnAddr, closeTurn := createTURNServer(t, ice.SchemeTypeTURN) - dialer, err := DialWebsocket(context.Background(), connectAddr, []webrtc.ICEServer{{ - URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, - Username: "example", - Credential: testPass, - CredentialType: webrtc.ICECredentialTypePassword, - }}) + dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ + ICEServers: []webrtc.ICEServer{{ + URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, + Username: "example", + Credential: testPass, + CredentialType: webrtc.ICECredentialTypePassword, + }}, + }) if err != nil { t.Error(err) return @@ -102,7 +106,7 @@ func TestDial(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr) + _, err := Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return @@ -141,7 +145,7 @@ func TestDial(t *testing.T) { }() connectAddr, listenAddr := createDumbBroker(t) - _, err = Listen(context.Background(), listenAddr) + _, err = Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return @@ -180,7 +184,7 @@ func TestDial(t *testing.T) { _, _ = listener.Accept() }() connectAddr, listenAddr := createDumbBroker(t) - srv, err := Listen(context.Background(), listenAddr) + srv, err := Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return @@ -207,7 +211,7 @@ func TestDial(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr) + _, err := Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return @@ -241,18 +245,20 @@ func TestDial(t *testing.T) { }() connectAddr, listenAddr := createDumbBroker(t) - _, err = Listen(context.Background(), listenAddr) + _, err = Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return } turnAddr, closeTurn := createTURNServer(t, ice.SchemeTypeTURN) - dialer, err := DialWebsocket(context.Background(), connectAddr, []webrtc.ICEServer{{ - URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, - Username: "example", - Credential: testPass, - CredentialType: webrtc.ICECredentialTypePassword, - }}) + dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ + ICEServers: []webrtc.ICEServer{{ + URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, + Username: "example", + Credential: testPass, + CredentialType: webrtc.ICECredentialTypePassword, + }}, + }) if err != nil { t.Error(err) return @@ -276,7 +282,7 @@ func TestDial(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr) + _, err := Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return @@ -327,7 +333,7 @@ func BenchmarkThroughput(b *testing.B) { } }() connectAddr, listenAddr := createDumbBroker(b) - _, err = Listen(context.Background(), listenAddr) + _, err = Listen(context.Background(), listenAddr, nil) if err != nil { b.Error(err) return diff --git a/wsnet/listen.go b/wsnet/listen.go index 3c7c3b3e..b29bbdb3 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/yamux" "github.com/pion/webrtc/v3" + "golang.org/x/net/proxy" "nhooyr.io/websocket" "cdr.dev/coder-cli/coder-sdk" @@ -39,10 +40,11 @@ type DialChannelResponse struct { // Listen connects to the broker proxies connections to the local net. // Close will end all RTC connections. -func Listen(ctx context.Context, broker string) (io.Closer, error) { +func Listen(ctx context.Context, broker string, tcpProxy proxy.Dialer) (io.Closer, error) { l := &listener{ broker: broker, connClosers: make([]io.Closer, 0), + tcpProxy: tcpProxy, } // We do a one-off dial outside of the loop to ensure the initial // connection is successful. If not, there's likely an error the @@ -83,7 +85,8 @@ func Listen(ctx context.Context, broker string) (io.Closer, error) { } type listener struct { - broker string + broker string + tcpProxy proxy.Dialer acceptError error ws *websocket.Conn @@ -186,13 +189,18 @@ func (l *listener) negotiate(conn net.Conn) { return } for _, server := range msg.Servers { + if server.Username == TURNProxyICECandidate().Username { + // This candidate is only used when proxying, + // so it will not validate. + continue + } err = DialICE(server, nil) if err != nil { closeError(fmt.Errorf("dial server %+v: %w", server.URLs, err)) return } } - rtc, err = newPeerConnection(msg.Servers) + rtc, err = newPeerConnection(msg.Servers, l.tcpProxy) if err != nil { closeError(err) return @@ -326,7 +334,7 @@ func (l *listener) handle(msg BrokerMessage) func(dc *webrtc.DataChannel) { } // Must wrap the data channel inside this connection // for buffering from the dialed endpoint to the client. - co := &conn{ + co := &dataChannelConn{ addr: nil, dc: dc, rw: rw, diff --git a/wsnet/listen_test.go b/wsnet/listen_test.go index 45519b92..47b856c3 100644 --- a/wsnet/listen_test.go +++ b/wsnet/listen_test.go @@ -45,7 +45,7 @@ func TestListen(t *testing.T) { addr := listener.Addr() broker := fmt.Sprintf("http://%s/", addr.String()) - _, err = Listen(context.Background(), broker) + _, err = Listen(context.Background(), broker, nil) if err != nil { t.Error(err) return diff --git a/wsnet/rtc.go b/wsnet/rtc.go index e8b5eab3..05c04f1b 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -17,6 +17,7 @@ import ( "github.com/pion/logging" "github.com/pion/turn/v2" "github.com/pion/webrtc/v3" + "golang.org/x/net/proxy" ) var ( @@ -154,7 +155,7 @@ func dialICEURL(server webrtc.ICEServer, rawURL string, options *DialICEOptions) } // Generalizes creating a new peer connection with consistent options. -func newPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, error) { +func newPeerConnection(servers []webrtc.ICEServer, dialer proxy.Dialer) (*webrtc.PeerConnection, error) { se := webrtc.SettingEngine{} se.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeUDP4}) se.SetSrflxAcceptanceMinWait(0) @@ -164,15 +165,21 @@ func newPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, erro lf.DefaultLogLevel = logging.LogLevelDisabled se.LoggerFactory = lf + // Enables tunneling of TURN traffic through an arbitrary proxy. + // We proxy TURN over a WebSocket to reduce deployment complexity. + if dialer != nil { + se.SetICEProxyDialer(dialer) + } + transportPolicy := webrtc.ICETransportPolicyAll // If one server is provided and we know it's TURN, we can set the // relay acceptable so the connection starts immediately. if len(servers) == 1 { server := servers[0] - if server.Credential != nil && len(server.URLs) == 1 { + if len(server.URLs) == 1 { url, err := ice.ParseURL(server.URLs[0]) - if err == nil && url.Proto == ice.ProtoTypeTCP { + if err == nil && server.Credential != nil && url.Proto == ice.ProtoTypeTCP { se.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6}) se.SetRelayAcceptanceMinWait(0) } From 0b4b4fa0446632320846b84902926a0a06cbed5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Jul 2021 12:39:57 -0700 Subject: [PATCH 063/128] chore: bump actions/cache from 1 to 2 (#385) Bumps [actions/cache](https://github.com/actions/cache) from 1 to 2. - [Release notes](https://github.com/actions/cache/releases) - [Commits](https://github.com/actions/cache/compare/v1...v2) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jonathan Yu --- .github/workflows/integration.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 1c0d03da..e7abedaa 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -13,7 +13,7 @@ jobs: CODER_PASSWORD: ${{ secrets.CODER_PASSWORD }} steps: - uses: actions/checkout@v1 - - uses: actions/cache@v1 + - uses: actions/cache@v2 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} From dd00acd09e28ef2745f05ecdd3e5995db5d0fcf7 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 16 Jul 2021 18:40:08 -0500 Subject: [PATCH 064/128] Add satellite commands (#387) * Add satellite commands * lint and docs * docs * better yes check * better domain * more docs * add not found error * fix types --- coder-sdk/interface.go | 9 ++ coder-sdk/satellite.go | 51 ++++++++ docs/coder.md | 1 + docs/coder_satellites.md | 27 ++++ docs/coder_satellites_create.md | 36 ++++++ docs/coder_satellites_ls.md | 35 +++++ docs/coder_satellites_rm.md | 35 +++++ internal/cmd/cmd.go | 1 + internal/cmd/satellites.go | 222 ++++++++++++++++++++++++++++++++ 9 files changed, 417 insertions(+) create mode 100644 coder-sdk/satellite.go create mode 100644 docs/coder_satellites.md create mode 100644 docs/coder_satellites_create.md create mode 100644 docs/coder_satellites_ls.md create mode 100644 docs/coder_satellites_rm.md create mode 100644 internal/cmd/satellites.go diff --git a/coder-sdk/interface.go b/coder-sdk/interface.go index e439ca32..dafb2114 100644 --- a/coder-sdk/interface.go +++ b/coder-sdk/interface.go @@ -241,4 +241,13 @@ type Client interface { // SetPolicyTemplate sets the workspace policy template SetPolicyTemplate(ctx context.Context, templateID string, templateScope TemplateScope, dryRun bool) (*SetPolicyTemplateResponse, error) + + // satellites fetches all satellitess known to the Coder control plane. + Satellites(ctx context.Context) ([]Satellite, error) + + // CreateSatellite creates a new satellite entity. + CreateSatellite(ctx context.Context, req CreateSatelliteReq) (*Satellite, error) + + // DeleteSatelliteByID deletes a satellite entity from the Coder control plane. + DeleteSatelliteByID(ctx context.Context, id string) error } diff --git a/coder-sdk/satellite.go b/coder-sdk/satellite.go new file mode 100644 index 00000000..975ee32d --- /dev/null +++ b/coder-sdk/satellite.go @@ -0,0 +1,51 @@ +package coder + +import ( + "context" + "net/http" +) + +type Satellite struct { + ID string `json:"id"` + Name string `json:"name"` + Fingerprint string `json:"fingerprint"` +} + +type satellites struct { + Data []Satellite `json:"data"` +} + +type createSatelliteResponse struct { + Data Satellite `json:"data"` +} + +// Satellites fetches all satellitess known to the Coder control plane. +func (c *DefaultClient) Satellites(ctx context.Context) ([]Satellite, error) { + var res satellites + err := c.requestBody(ctx, http.MethodGet, "/api/private/satellites", nil, &res) + if err != nil { + return nil, err + } + return res.Data, nil +} + +// CreateSatelliteReq defines the request parameters for creating a new satellite entity. +type CreateSatelliteReq struct { + Name string `json:"name"` + PublicKey string `json:"public_key"` +} + +// CreateSatellite creates a new satellite entity. +func (c *DefaultClient) CreateSatellite(ctx context.Context, req CreateSatelliteReq) (*Satellite, error) { + var res createSatelliteResponse + err := c.requestBody(ctx, http.MethodPost, "/api/private/satellites", req, &res) + if err != nil { + return nil, err + } + return &res.Data, nil +} + +// DeleteSatelliteByID deletes a satellite entity from the Coder control plane. +func (c *DefaultClient) DeleteSatelliteByID(ctx context.Context, id string) error { + return c.requestBody(ctx, http.MethodDelete, "/api/private/satellites/"+id, nil, nil) +} diff --git a/docs/coder.md b/docs/coder.md index ab6254e8..17e7fa7f 100644 --- a/docs/coder.md +++ b/docs/coder.md @@ -16,6 +16,7 @@ coder provides a CLI for working with an existing Coder installation * [coder images](coder_images.md) - Manage Coder images * [coder login](coder_login.md) - Authenticate this client for future operations * [coder logout](coder_logout.md) - Remove local authentication credentials if any exist +* [coder satellites](coder_satellites.md) - Interact with Coder satellite deployments * [coder ssh](coder_ssh.md) - Enter a shell of execute a command over SSH into a Coder workspace * [coder sync](coder_sync.md) - Establish a one way directory sync to a Coder workspace * [coder tokens](coder_tokens.md) - manage Coder API tokens for the active user diff --git a/docs/coder_satellites.md b/docs/coder_satellites.md new file mode 100644 index 00000000..2eaac5b9 --- /dev/null +++ b/docs/coder_satellites.md @@ -0,0 +1,27 @@ +## coder satellites + +Interact with Coder satellite deployments + +### Synopsis + +Perform operations on the Coder satellites for the platform. + +### Options + +``` + -h, --help help for satellites +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation +* [coder satellites create](coder_satellites_create.md) - create a new satellite. +* [coder satellites ls](coder_satellites_ls.md) - list satellites. +* [coder satellites rm](coder_satellites_rm.md) - remove a satellite. + diff --git a/docs/coder_satellites_create.md b/docs/coder_satellites_create.md new file mode 100644 index 00000000..9ab18362 --- /dev/null +++ b/docs/coder_satellites_create.md @@ -0,0 +1,36 @@ +## coder satellites create + +create a new satellite. + +### Synopsis + +Create a new Coder satellite. + +``` +coder satellites create [name] [satellite_access_url] [flags] +``` + +### Examples + +``` +# create a new satellite + +coder satellites create eu-west https://eu-west.coder.com +``` + +### Options + +``` + -h, --help help for create +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder satellites](coder_satellites.md) - Interact with Coder satellite deployments + diff --git a/docs/coder_satellites_ls.md b/docs/coder_satellites_ls.md new file mode 100644 index 00000000..d2153685 --- /dev/null +++ b/docs/coder_satellites_ls.md @@ -0,0 +1,35 @@ +## coder satellites ls + +list satellites. + +### Synopsis + +List all Coder workspace satellites. + +``` +coder satellites ls [flags] +``` + +### Examples + +``` +# list satellites +coder satellites ls +``` + +### Options + +``` + -h, --help help for ls +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder satellites](coder_satellites.md) - Interact with Coder satellite deployments + diff --git a/docs/coder_satellites_rm.md b/docs/coder_satellites_rm.md new file mode 100644 index 00000000..44669f6f --- /dev/null +++ b/docs/coder_satellites_rm.md @@ -0,0 +1,35 @@ +## coder satellites rm + +remove a satellite. + +### Synopsis + +Remove an existing Coder satellite by name. + +``` +coder satellites rm [satellite_name] [flags] +``` + +### Examples + +``` +# remove an existing satellite by name +coder satellites rm my-satellite +``` + +### Options + +``` + -h, --help help for rm +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder satellites](coder_satellites.md) - Interact with Coder satellite deployments + diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 38e5ae4f..4ad33209 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -40,6 +40,7 @@ func Make() *cobra.Command { genDocsCmd(app), agentCmd(), tunnelCmd(), + satellitesCmd(), ) app.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show verbose output") return app diff --git a/internal/cmd/satellites.go b/internal/cmd/satellites.go new file mode 100644 index 00000000..982451f7 --- /dev/null +++ b/internal/cmd/satellites.go @@ -0,0 +1,222 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + + "cdr.dev/coder-cli/internal/x/xcobra" + + "github.com/spf13/cobra" + "golang.org/x/xerrors" + + "cdr.dev/coder-cli/coder-sdk" + "cdr.dev/coder-cli/pkg/clog" + "cdr.dev/coder-cli/pkg/tablewriter" +) + +const ( + satelliteKeyPath = "/api/private/satellites/key" +) + +type satelliteKeyResponse struct { + Key string `json:"key"` + Fingerprint string `json:"fingerprint"` +} + +func satellitesCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "satellites", + Short: "Interact with Coder satellite deployments", + Long: "Perform operations on the Coder satellites for the platform.", + } + + cmd.AddCommand( + createSatelliteCmd(), + listSatellitesCmd(), + deleteSatelliteCmd(), + ) + return cmd +} + +func createSatelliteCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "create [name] [satellite_access_url]", + Args: xcobra.ExactArgs(2), + Short: "create a new satellite.", + Long: "Create a new Coder satellite.", + Example: `# create a new satellite + +coder satellites create eu-west https://eu-west.coder.com`, + RunE: func(cmd *cobra.Command, args []string) error { + var ( + ctx = cmd.Context() + name = args[0] + accessURL = args[1] + ) + + client, err := newClient(ctx, true) + if err != nil { + return xerrors.Errorf("making coder client: %w", err) + } + + sURL, err := url.Parse(accessURL) + if err != nil { + return xerrors.Errorf("parsing satellite access url: %w", err) + } + sURL.Path = satelliteKeyPath + + // Create the http request. + req, err := http.NewRequestWithContext(ctx, http.MethodGet, sURL.String(), nil) + if err != nil { + return xerrors.Errorf("create satellite request: %w", err) + } + res, err := http.DefaultClient.Do(req) + if err != nil { + return xerrors.Errorf("doing satellite request: %w", err) + } + defer func() { _ = res.Body.Close() }() + + if res.StatusCode > 299 { + return fmt.Errorf("unexpected status code %d: %+v", res.StatusCode, res) + } + + var keyRes satelliteKeyResponse + if err := json.NewDecoder(res.Body).Decode(&keyRes); err != nil { + return xerrors.Errorf("decode response body: %w", err) + } + + if keyRes.Key == "" { + return xerrors.New("key field empty in response") + } + if keyRes.Fingerprint == "" { + return xerrors.New("fingerprint field empty in response") + } + + fmt.Printf(`The following satellite will be created: +Name: %s + +Public Key: +%s + +Fingerprint: +%s + +Do you wish to continue? (y/n) +`, name, keyRes.Key, keyRes.Fingerprint) + err = getConfirmation() + if err != nil { + return err + } + + _, err = client.CreateSatellite(ctx, coder.CreateSatelliteReq{ + Name: name, + PublicKey: keyRes.Key, + }) + if err != nil { + return xerrors.Errorf("making create satellite request: %w", err) + } + + clog.LogSuccess(fmt.Sprintf("satellite %s successfully created", name)) + + return nil + }, + } + + return cmd +} + +func getConfirmation() error { + var response string + + _, err := fmt.Scanln(&response) + if err != nil { + return xerrors.Errorf("scan line: %w", err) + } + + response = strings.ToLower(strings.TrimSpace(response)) + if response != "y" && response != "yes" { + return xerrors.New("request canceled") + } + + return nil +} + +func listSatellitesCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "ls", + Short: "list satellites.", + Long: "List all Coder workspace satellites.", + Example: `# list satellites +coder satellites ls`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + client, err := newClient(ctx, true) + if err != nil { + return xerrors.Errorf("making coder client: %w", err) + } + + sats, err := client.Satellites(ctx) + if err != nil { + return xerrors.Errorf("get satellites request: %w", err) + } + + if len(sats) == 0 { + return xerrors.Errorf("no satellites found") + } + + err = tablewriter.WriteTable(cmd.OutOrStdout(), len(sats), func(i int) interface{} { + return sats[i] + }) + if err != nil { + return xerrors.Errorf("write table: %w", err) + } + + return nil + }, + } + return cmd +} + +func deleteSatelliteCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "rm [satellite_name]", + Args: xcobra.ExactArgs(1), + Short: "remove a satellite.", + Long: "Remove an existing Coder satellite by name.", + Example: `# remove an existing satellite by name +coder satellites rm my-satellite`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + name := args[0] + + client, err := newClient(ctx, true) + if err != nil { + return err + } + + sats, err := client.Satellites(ctx) + if err != nil { + return xerrors.Errorf("get satellites request: %w", err) + } + + for _, sat := range sats { + if sat.Name == name { + err = client.DeleteSatelliteByID(ctx, sat.ID) + if err != nil { + return xerrors.Errorf("delete satellites request: %w", err) + } + clog.LogSuccess(fmt.Sprintf("satellite %s successfully deleted", name)) + + return nil + } + } + + return xerrors.Errorf("no satellite found by name '%s'", name) + }, + } + return cmd +} From 1294dd98193167d6294aacfe84492cb6429f65d6 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 19 Jul 2021 10:43:52 -0600 Subject: [PATCH 065/128] fix: Add TURN URL to handshake message for custom proxying URLs (#389) * fix: Enable TURN over HTTP with proper callback * Remove TURN proxy dialer * Don't use credential to authenticate --- internal/cmd/agent.go | 2 +- internal/cmd/tunnel.go | 5 ++++- wsnet/conn.go | 20 ++++++-------------- wsnet/dial.go | 36 ++++++++++++++++++++++++------------ wsnet/dial_test.go | 18 +++++++++--------- wsnet/listen.go | 29 +++++++++++++++++++++-------- wsnet/listen_test.go | 2 +- wsnet/proto.go | 6 ++++-- 8 files changed, 70 insertions(+), 48 deletions(-) diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go index c19bdfee..7563ee4b 100644 --- a/internal/cmd/agent.go +++ b/internal/cmd/agent.go @@ -73,7 +73,7 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx } } - listener, err := wsnet.Listen(context.Background(), wsnet.ListenEndpoint(u, token), wsnet.TURNProxyWebSocket(u, token)) + listener, err := wsnet.Listen(context.Background(), wsnet.ListenEndpoint(u, token), token) if err != nil { return xerrors.Errorf("listen: %w", err) } diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index 7b14cf33..9c12dd37 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -11,6 +11,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" + "github.com/pion/webrtc/v3" "github.com/spf13/cobra" "golang.org/x/xerrors" @@ -107,7 +108,9 @@ func (c *tunnneler) start(ctx context.Context) error { ctx, wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token), &wsnet.DialOptions{ - TURNProxy: wsnet.TURNProxyWebSocket(c.brokerAddr, c.token), + TURNProxyAuthToken: c.token, + TURNProxyURL: c.brokerAddr, + ICEServers: []webrtc.ICEServer{wsnet.TURNProxyICECandidate()}, }, ) if err != nil { diff --git a/wsnet/conn.go b/wsnet/conn.go index 608c5c70..5b863f04 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -11,14 +11,14 @@ import ( "github.com/pion/datachannel" "github.com/pion/webrtc/v3" - "golang.org/x/net/proxy" "nhooyr.io/websocket" "cdr.dev/coder-cli/coder-sdk" ) const ( - httpScheme = "http" + httpScheme = "http" + turnProxyMagicUsername = "~magicalusername~" bufferedAmountLowThreshold uint64 = 512 * 1024 // 512 KB maxBufferedAmount uint64 = 1024 * 1024 // 1 MB @@ -46,25 +46,17 @@ func ConnectEndpoint(baseURL *url.URL, workspace, token string) string { return fmt.Sprintf("%s://%s%s%s%s%s", wsScheme, baseURL.Host, "/api/private/envagent/", workspace, "/connect?session_token=", token) } -// TURNWebSocketICECandidate returns a valid relay ICEServer that can be used to -// trigger a TURNWebSocketDialer. +// TURNWebSocketICECandidate returns a fake TCP relay ICEServer. +// It's used to trigger the ICEProxyDialer. func TURNProxyICECandidate() webrtc.ICEServer { return webrtc.ICEServer{ URLs: []string{"turn:127.0.0.1:3478?transport=tcp"}, - Username: "~magicalusername~", - Credential: "~magicalpassword~", + Username: turnProxyMagicUsername, + Credential: turnProxyMagicUsername, CredentialType: webrtc.ICECredentialTypePassword, } } -// TURNWebSocketDialer proxies all TURN traffic through a WebSocket. -func TURNProxyWebSocket(baseURL *url.URL, token string) proxy.Dialer { - return &turnProxyDialer{ - baseURL: baseURL, - token: token, - } -} - // Proxies all TURN ICEServer traffic through this dialer. // References Coder APIs with a specific token. type turnProxyDialer struct { diff --git a/wsnet/dial.go b/wsnet/dial.go index 362bbab9..97d49827 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net" + "net/url" "sync" "time" @@ -24,10 +25,13 @@ type DialOptions struct { // See: https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration/iceServers ICEServers []webrtc.ICEServer - // TURNProxy is a function used to proxy all TURN traffic. - // If specified without ICEServers, `TURNProxyICECandidate` - // will be used. - TURNProxy proxy.Dialer + // TURNProxyAuthToken is used to authenticate a TURN proxy request. + TURNProxyAuthToken string + + // TURNProxyURL is the URL to proxy all TURN data through. + // This URL is sent to the listener during handshake so both + // ends connect to the same TURN endpoint. + TURNProxyURL *url.URL } // DialWebsocket dials the broker with a WebSocket and negotiates a connection. @@ -59,13 +63,15 @@ func Dial(conn net.Conn, options *DialOptions) (*Dialer, error) { if options.ICEServers == nil { options.ICEServers = []webrtc.ICEServer{} } - // If the TURNProxy is specified and ICEServers aren't, - // it's safe to assume we can inject the default proxy candidate. - if len(options.ICEServers) == 0 && options.TURNProxy != nil { - options.ICEServers = []webrtc.ICEServer{TURNProxyICECandidate()} - } - rtc, err := newPeerConnection(options.ICEServers, options.TURNProxy) + var turnProxy proxy.Dialer + if options.TURNProxyURL != nil { + turnProxy = &turnProxyDialer{ + baseURL: options.TURNProxyURL, + token: options.TURNProxyAuthToken, + } + } + rtc, err := newPeerConnection(options.ICEServers, turnProxy) if err != nil { return nil, fmt.Errorf("create peer connection: %w", err) } @@ -89,9 +95,15 @@ func Dial(conn net.Conn, options *DialOptions) (*Dialer, error) { return nil, fmt.Errorf("set local offer: %w", err) } + var turnProxyURL string + if options.TURNProxyURL != nil { + turnProxyURL = options.TURNProxyURL.String() + } + offerMessage, err := json.Marshal(&BrokerMessage{ - Offer: &offer, - Servers: options.ICEServers, + Offer: &offer, + Servers: options.ICEServers, + TURNProxyURL: turnProxyURL, }) if err != nil { return nil, fmt.Errorf("marshal offer message: %w", err) diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 6ad27866..91b7d0a2 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -55,7 +55,7 @@ func TestDial(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr, nil) + _, err := Listen(context.Background(), listenAddr, "") if err != nil { t.Error(err) return @@ -75,7 +75,7 @@ func TestDial(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr, nil) + _, err := Listen(context.Background(), listenAddr, "") if err != nil { t.Error(err) return @@ -106,7 +106,7 @@ func TestDial(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr, nil) + _, err := Listen(context.Background(), listenAddr, "") if err != nil { t.Error(err) return @@ -145,7 +145,7 @@ func TestDial(t *testing.T) { }() connectAddr, listenAddr := createDumbBroker(t) - _, err = Listen(context.Background(), listenAddr, nil) + _, err = Listen(context.Background(), listenAddr, "") if err != nil { t.Error(err) return @@ -184,7 +184,7 @@ func TestDial(t *testing.T) { _, _ = listener.Accept() }() connectAddr, listenAddr := createDumbBroker(t) - srv, err := Listen(context.Background(), listenAddr, nil) + srv, err := Listen(context.Background(), listenAddr, "") if err != nil { t.Error(err) return @@ -211,7 +211,7 @@ func TestDial(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr, nil) + _, err := Listen(context.Background(), listenAddr, "") if err != nil { t.Error(err) return @@ -245,7 +245,7 @@ func TestDial(t *testing.T) { }() connectAddr, listenAddr := createDumbBroker(t) - _, err = Listen(context.Background(), listenAddr, nil) + _, err = Listen(context.Background(), listenAddr, "") if err != nil { t.Error(err) return @@ -282,7 +282,7 @@ func TestDial(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr, nil) + _, err := Listen(context.Background(), listenAddr, "") if err != nil { t.Error(err) return @@ -333,7 +333,7 @@ func BenchmarkThroughput(b *testing.B) { } }() connectAddr, listenAddr := createDumbBroker(b) - _, err = Listen(context.Background(), listenAddr, nil) + _, err = Listen(context.Background(), listenAddr, "") if err != nil { b.Error(err) return diff --git a/wsnet/listen.go b/wsnet/listen.go index b29bbdb3..e159b6e3 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net" + "net/url" "sync" "time" @@ -40,11 +41,11 @@ type DialChannelResponse struct { // Listen connects to the broker proxies connections to the local net. // Close will end all RTC connections. -func Listen(ctx context.Context, broker string, tcpProxy proxy.Dialer) (io.Closer, error) { +func Listen(ctx context.Context, broker string, turnProxyAuthToken string) (io.Closer, error) { l := &listener{ - broker: broker, - connClosers: make([]io.Closer, 0), - tcpProxy: tcpProxy, + broker: broker, + connClosers: make([]io.Closer, 0), + turnProxyAuthToken: turnProxyAuthToken, } // We do a one-off dial outside of the loop to ensure the initial // connection is successful. If not, there's likely an error the @@ -85,8 +86,8 @@ func Listen(ctx context.Context, broker string, tcpProxy proxy.Dialer) (io.Close } type listener struct { - broker string - tcpProxy proxy.Dialer + broker string + turnProxyAuthToken string acceptError error ws *websocket.Conn @@ -189,7 +190,7 @@ func (l *listener) negotiate(conn net.Conn) { return } for _, server := range msg.Servers { - if server.Username == TURNProxyICECandidate().Username { + if server.Username == turnProxyMagicUsername { // This candidate is only used when proxying, // so it will not validate. continue @@ -200,7 +201,19 @@ func (l *listener) negotiate(conn net.Conn) { return } } - rtc, err = newPeerConnection(msg.Servers, l.tcpProxy) + var turnProxy proxy.Dialer + if msg.TURNProxyURL != "" { + u, err := url.Parse(msg.TURNProxyURL) + if err != nil { + closeError(fmt.Errorf("parse turn proxy url: %w", err)) + return + } + turnProxy = &turnProxyDialer{ + baseURL: u, + token: l.turnProxyAuthToken, + } + } + rtc, err = newPeerConnection(msg.Servers, turnProxy) if err != nil { closeError(err) return diff --git a/wsnet/listen_test.go b/wsnet/listen_test.go index 47b856c3..2c5ba35f 100644 --- a/wsnet/listen_test.go +++ b/wsnet/listen_test.go @@ -45,7 +45,7 @@ func TestListen(t *testing.T) { addr := listener.Addr() broker := fmt.Sprintf("http://%s/", addr.String()) - _, err = Listen(context.Background(), broker, nil) + _, err = Listen(context.Background(), broker, "") if err != nil { t.Error(err) return diff --git a/wsnet/proto.go b/wsnet/proto.go index 754fffac..feb4d126 100644 --- a/wsnet/proto.go +++ b/wsnet/proto.go @@ -49,8 +49,10 @@ func (p DialPolicy) permits(network, host string, port uint16) bool { // sides can begin exchanging candidates. type BrokerMessage struct { // Dialer -> Listener - Offer *webrtc.SessionDescription `json:"offer"` - Servers []webrtc.ICEServer `json:"servers"` + Offer *webrtc.SessionDescription `json:"offer"` + Servers []webrtc.ICEServer `json:"servers"` + TURNProxyURL string `json:"turn_proxy_url"` + // Policies denote which addresses the client can dial. If empty or nil, all // addresses are permitted. Policies []DialPolicy `json:"ports"` From 4de99d5c85062b3f7abfe5ab411bdb7471f3efa4 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 21 Jul 2021 02:55:16 +1000 Subject: [PATCH 066/128] Add logs to wsnet listener (#388) --- go.mod | 1 + internal/cmd/agent.go | 24 +++-- wsnet/dial_test.go | 221 ++++++++++++++++-------------------------- wsnet/listen.go | 96 ++++++++++++++++-- wsnet/listen_test.go | 45 +++------ 5 files changed, 199 insertions(+), 188 deletions(-) diff --git a/go.mod b/go.mod index 46a9d7ce..33895cc2 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.2.1 + github.com/stretchr/testify v1.7.0 golang.org/x/net v0.0.0-20210614182718-04defd469f4e golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go index 7563ee4b..29f65b21 100644 --- a/internal/cmd/agent.go +++ b/internal/cmd/agent.go @@ -1,13 +1,15 @@ package cmd import ( - "context" - "log" "net/url" "os" "os/signal" "syscall" + // We use slog here since agent runs in the background and we can benefit + // from structured logging. + "cdr.dev/slog" + "cdr.dev/slog/sloggers/sloghuman" "github.com/spf13/cobra" "golang.org/x/xerrors" @@ -46,7 +48,10 @@ coder agent start coder agent start --coder-url https://my-coder.com --token xxxx-xxxx `, RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() + var ( + ctx = cmd.Context() + log = slog.Make(sloghuman.Sink(os.Stderr)).Leveled(slog.LevelDebug) + ) if coderURL == "" { var ok bool coderURL, ok = os.LookupEnv("CODER_URL") @@ -73,20 +78,23 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx } } - listener, err := wsnet.Listen(context.Background(), wsnet.ListenEndpoint(u, token), token) + log.Info(ctx, "starting wsnet listener", slog.F("coder_access_url", u.String())) + listener, err := wsnet.Listen(ctx, log, wsnet.ListenEndpoint(u, token), token) if err != nil { return xerrors.Errorf("listen: %w", err) } + defer func() { + err := listener.Close() + if err != nil { + log.Error(ctx, "close listener", slog.Error(err)) + } + }() // Block until user sends SIGINT or SIGTERM sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs - if err = listener.Close(); err != nil { - log.Panic(err) - } - return nil }, } diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 91b7d0a2..fcd00ac7 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -1,7 +1,6 @@ package wsnet import ( - "bytes" "context" "crypto/rand" "errors" @@ -12,8 +11,11 @@ import ( "testing" "time" + "cdr.dev/slog/sloggers/slogtest" "github.com/pion/ice/v2" "github.com/pion/webrtc/v3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func ExampleDial_basic() { @@ -49,37 +51,30 @@ func ExampleDial_basic() { // You now have access to the proxied remote port in `conn`. } -// nolint:gocognit,gocyclo func TestDial(t *testing.T) { t.Run("Ping", func(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr, "") - if err != nil { - t.Error(err) - return - } + l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + require.NoError(t, err) + defer l.Close() + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) + err = dialer.Ping(context.Background()) - if err != nil { - t.Error(err) - } + require.NoError(t, err) }) t.Run("Ping Close", func(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr, "") - if err != nil { - t.Error(err) - return - } + l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + require.NoError(t, err) + defer l.Close() + turnAddr, closeTurn := createTURNServer(t, ice.SchemeTypeTURN) dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ ICEServers: []webrtc.ICEServer{{ @@ -89,86 +84,63 @@ func TestDial(t *testing.T) { CredentialType: webrtc.ICECredentialTypePassword, }}, }) - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) + _ = dialer.Ping(context.Background()) closeTurn() err = dialer.Ping(context.Background()) - if err != io.EOF { - t.Error(err) - return - } + assert.ErrorIs(t, err, io.EOF) }) t.Run("OPError", func(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr, "") - if err != nil { - t.Error(err) - return - } + l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + require.NoError(t, err) + defer l.Close() + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) - if err != nil { - t.Error(err) - } + require.NoError(t, err) + _, err = dialer.DialContext(context.Background(), "tcp", "localhost:100") - if err == nil { - t.Error("should have gotten err") - return - } - _, ok := err.(*net.OpError) - if !ok { - t.Error("invalid error type returned") - return - } + assert.Error(t, err) + + // Double pointer intended. + netErr := &net.OpError{} + assert.ErrorAs(t, err, &netErr) }) t.Run("Proxy", func(t *testing.T) { t.Parallel() listener, err := net.Listen("tcp", "0.0.0.0:0") - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) + msg := []byte("Hello!") go func() { conn, err := listener.Accept() - if err != nil { - t.Error(err) - } + require.NoError(t, err) + _, _ = conn.Write(msg) }() connectAddr, listenAddr := createDumbBroker(t) - _, err = Listen(context.Background(), listenAddr, "") - if err != nil { - t.Error(err) - return - } + l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + require.NoError(t, err) + defer l.Close() + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) + conn, err := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) + rec := make([]byte, len(msg)) _, err = conn.Read(rec) - if err != nil { - t.Error(err) - return - } - if !bytes.Equal(msg, rec) { - t.Error("bytes were different", string(msg), string(rec)) - } + require.NoError(t, err) + + assert.Equal(t, msg, rec) }) // Expect that we'd get an EOF on the server closing. @@ -176,80 +148,60 @@ func TestDial(t *testing.T) { t.Parallel() listener, err := net.Listen("tcp", "0.0.0.0:0") - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) go func() { _, _ = listener.Accept() }() + connectAddr, listenAddr := createDumbBroker(t) - srv, err := Listen(context.Background(), listenAddr, "") - if err != nil { - t.Error(err) - return - } + l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + require.NoError(t, err) + defer l.Close() + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) - if err != nil { - t.Error(err) - } + require.NoError(t, err) + conn, err := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) - if err != nil { - t.Error(err) - return - } - go srv.Close() + require.NoError(t, err) + + go l.Close() rec := make([]byte, 16) _, err = conn.Read(rec) - if !errors.Is(err, io.EOF) { - t.Error(err) - return - } + assert.ErrorIs(t, err, io.EOF) }) t.Run("Disconnect", func(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr, "") - if err != nil { - t.Error(err) - return - } + l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + require.NoError(t, err) + defer l.Close() + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) + err = dialer.Close() - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) + err = dialer.Ping(context.Background()) - if err != webrtc.ErrConnectionClosed { - t.Error(err) - } + assert.ErrorIs(t, err, webrtc.ErrConnectionClosed) }) t.Run("Disconnect DialContext", func(t *testing.T) { t.Parallel() tcpListener, err := net.Listen("tcp", "0.0.0.0:0") - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) go func() { _, _ = tcpListener.Accept() }() connectAddr, listenAddr := createDumbBroker(t) - _, err = Listen(context.Background(), listenAddr, "") - if err != nil { - t.Error(err) - return - } + l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + require.NoError(t, err) + defer l.Close() + turnAddr, closeTurn := createTURNServer(t, ice.SchemeTypeTURN) dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ ICEServers: []webrtc.ICEServer{{ @@ -259,42 +211,32 @@ func TestDial(t *testing.T) { CredentialType: webrtc.ICECredentialTypePassword, }}, }) - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) + conn, err := dialer.DialContext(context.Background(), "tcp", tcpListener.Addr().String()) - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) + // Close the TURN server before reading... // WebRTC connections take a few seconds to timeout. closeTurn() _, err = conn.Read(make([]byte, 16)) - if err != io.EOF { - t.Error(err) - return - } + assert.ErrorIs(t, err, io.EOF) }) t.Run("Closed", func(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr, "") - if err != nil { - t.Error(err) - return - } + l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + require.NoError(t, err) + defer l.Close() + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) go func() { _ = dialer.Close() }() + select { case <-dialer.Closed(): case <-time.NewTimer(time.Second).C: @@ -333,11 +275,12 @@ func BenchmarkThroughput(b *testing.B) { } }() connectAddr, listenAddr := createDumbBroker(b) - _, err = Listen(context.Background(), listenAddr, "") + l, err := Listen(context.Background(), slogtest.Make(b, nil), listenAddr, "") if err != nil { b.Error(err) return } + defer l.Close() dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { diff --git a/wsnet/listen.go b/wsnet/listen.go index e159b6e3..02f13f41 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -9,6 +9,7 @@ import ( "net" "net/url" "sync" + "sync/atomic" "time" "github.com/hashicorp/yamux" @@ -16,6 +17,8 @@ import ( "golang.org/x/net/proxy" "nhooyr.io/websocket" + "cdr.dev/slog" + "cdr.dev/coder-cli/coder-sdk" ) @@ -41,12 +44,15 @@ type DialChannelResponse struct { // Listen connects to the broker proxies connections to the local net. // Close will end all RTC connections. -func Listen(ctx context.Context, broker string, turnProxyAuthToken string) (io.Closer, error) { +func Listen(ctx context.Context, log slog.Logger, broker string, turnProxyAuthToken string) (io.Closer, error) { l := &listener{ + log: log, broker: broker, connClosers: make([]io.Closer, 0), + closed: make(chan struct{}, 1), turnProxyAuthToken: turnProxyAuthToken, } + // We do a one-off dial outside of the loop to ensure the initial // connection is successful. If not, there's likely an error the // user needs to act on. @@ -57,7 +63,17 @@ func Listen(ctx context.Context, broker string, turnProxyAuthToken string) (io.C go func() { for { err := <-ch + select { + case _, ok := <-l.closed: + if !ok { + return + } + default: + } + if errors.Is(err, io.EOF) || errors.Is(err, yamux.ErrKeepAliveTimeout) { + l.log.Warn(ctx, "disconnected from broker", slog.Error(err)) + // If we hit an EOF, then the connection to the broker // was interrupted. We'll take a short break then dial // again. @@ -89,16 +105,21 @@ type listener struct { broker string turnProxyAuthToken string + log slog.Logger acceptError error ws *websocket.Conn connClosers []io.Closer connClosersMut sync.Mutex + closed chan struct{} + nextConnNumber int64 } func (l *listener) dial(ctx context.Context) (<-chan error, error) { + l.log.Info(ctx, "connecting to broker", slog.F("broker_url", l.broker)) if l.ws != nil { _ = l.ws.Close(websocket.StatusNormalClosure, "new connection inbound") } + conn, resp, err := websocket.Dial(ctx, l.broker, nil) if err != nil { if resp != nil { @@ -107,6 +128,7 @@ func (l *listener) dial(ctx context.Context) (<-chan error, error) { return nil, err } l.ws = conn + nconn := websocket.NetConn(ctx, conn, websocket.MessageBinary) config := yamux.DefaultConfig() config.LogOutput = io.Discard @@ -114,6 +136,8 @@ func (l *listener) dial(ctx context.Context) (<-chan error, error) { if err != nil { return nil, fmt.Errorf("create multiplex: %w", err) } + + l.log.Info(ctx, "broker connection established") errCh := make(chan error) go func() { defer close(errCh) @@ -123,9 +147,10 @@ func (l *listener) dial(ctx context.Context) (<-chan error, error) { errCh <- err break } - go l.negotiate(conn) + go l.negotiate(ctx, conn) } }() + return errCh, nil } @@ -133,9 +158,10 @@ func (l *listener) dial(ctx context.Context) (<-chan error, error) { // This functions control-flow is important to readability, // so the cognitive overload linter has been disabled. // nolint:gocognit,nestif -func (l *listener) negotiate(conn net.Conn) { +func (l *listener) negotiate(ctx context.Context, conn net.Conn) { var ( err error + id = atomic.AddInt64(&l.nextConnNumber, 1) decoder = json.NewDecoder(conn) rtc *webrtc.PeerConnection // If candidates are sent before an offer, we place them here. @@ -145,6 +171,8 @@ func (l *listener) negotiate(conn net.Conn) { // Sends the error provided then closes the connection. // If RTC isn't connected, we'll close it. closeError = func(err error) { + l.log.Warn(ctx, "negotiation error, closing connection", slog.Error(err)) + d, _ := json.Marshal(&BrokerMessage{ Error: err.Error(), }) @@ -159,6 +187,9 @@ func (l *listener) negotiate(conn net.Conn) { } ) + ctx = slog.With(ctx, slog.F("conn_id", id)) + l.log.Info(ctx, "accepted new session from broker connection, negotiating") + for { var msg BrokerMessage err = decoder.Decode(&msg) @@ -166,6 +197,7 @@ func (l *listener) negotiate(conn net.Conn) { closeError(err) return } + l.log.Debug(ctx, "received broker message", slog.F("msg", msg)) if msg.Candidate != "" { c := webrtc.ICECandidateInit{ @@ -177,6 +209,7 @@ func (l *listener) negotiate(conn net.Conn) { continue } + l.log.Debug(ctx, "adding ICE candidate", slog.F("c", c)) err = rtc.AddICECandidate(c) if err != nil { closeError(fmt.Errorf("accept ice candidate: %w", err)) @@ -195,12 +228,15 @@ func (l *listener) negotiate(conn net.Conn) { // so it will not validate. continue } + + l.log.Debug(ctx, "validating ICE server", slog.F("s", server)) err = DialICE(server, nil) if err != nil { closeError(fmt.Errorf("dial server %+v: %w", server.URLs, err)) return } } + var turnProxy proxy.Dialer if msg.TURNProxyURL != "" { u, err := url.Parse(msg.TURNProxyURL) @@ -219,26 +255,33 @@ func (l *listener) negotiate(conn net.Conn) { return } rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { + l.log.Debug(ctx, "connection state change", slog.F("state", pcs.String())) if pcs == webrtc.PeerConnectionStateConnecting { return } _ = conn.Close() }) + flushCandidates := proxyICECandidates(rtc, conn) l.connClosersMut.Lock() l.connClosers = append(l.connClosers, rtc) l.connClosersMut.Unlock() - rtc.OnDataChannel(l.handle(msg)) + rtc.OnDataChannel(l.handle(ctx, msg)) + + l.log.Debug(ctx, "set remote description", slog.F("offer", *msg.Offer)) err = rtc.SetRemoteDescription(*msg.Offer) if err != nil { closeError(fmt.Errorf("apply offer: %w", err)) return } + answer, err := rtc.CreateAnswer(nil) if err != nil { closeError(fmt.Errorf("create answer: %w", err)) return } + + l.log.Debug(ctx, "set local description", slog.F("answer", answer)) err = rtc.SetLocalDescription(answer) if err != nil { closeError(fmt.Errorf("set local answer: %w", err)) @@ -246,13 +289,16 @@ func (l *listener) negotiate(conn net.Conn) { } flushCandidates() - data, err := json.Marshal(&BrokerMessage{ + bmsg := &BrokerMessage{ Answer: rtc.LocalDescription(), - }) + } + data, err := json.Marshal(bmsg) if err != nil { closeError(fmt.Errorf("marshal: %w", err)) return } + + l.log.Debug(ctx, "writing message", slog.F("msg", bmsg)) _, err = conn.Write(data) if err != nil { closeError(fmt.Errorf("write: %w", err)) @@ -260,6 +306,7 @@ func (l *listener) negotiate(conn net.Conn) { } for _, candidate := range pendingCandidates { + l.log.Debug(ctx, "adding pending ICE candidate", slog.F("c", candidate)) err = rtc.AddICECandidate(candidate) if err != nil { closeError(fmt.Errorf("add pending candidate: %w", err)) @@ -271,11 +318,13 @@ func (l *listener) negotiate(conn net.Conn) { } } -func (l *listener) handle(msg BrokerMessage) func(dc *webrtc.DataChannel) { +// nolint:gocognit +func (l *listener) handle(ctx context.Context, msg BrokerMessage) func(dc *webrtc.DataChannel) { return func(dc *webrtc.DataChannel) { if dc.Protocol() == controlChannel { // The control channel handles pings. dc.OnOpen(func() { + l.log.Debug(ctx, "control channel open") rw, err := dc.Detach() if err != nil { return @@ -283,7 +332,11 @@ func (l *listener) handle(msg BrokerMessage) func(dc *webrtc.DataChannel) { // We'll read and write back a single byte for ping/pongin'. d := make([]byte, 1) for { + l.log.Debug(ctx, "sending ping") _, err = rw.Read(d) + if err != nil { + l.log.Debug(ctx, "reading ping response failed", slog.Error(err)) + } if errors.Is(err, io.EOF) { return } @@ -296,7 +349,14 @@ func (l *listener) handle(msg BrokerMessage) func(dc *webrtc.DataChannel) { return } + ctx := slog.With(ctx, + slog.F("dc_id", dc.ID()), + slog.F("dc_label", dc.Label()), + slog.F("dc_proto", dc.Protocol()), + ) + dc.OnOpen(func() { + l.log.Info(ctx, "data channel opened") rw, err := dc.Detach() if err != nil { return @@ -304,17 +364,21 @@ func (l *listener) handle(msg BrokerMessage) func(dc *webrtc.DataChannel) { var init DialChannelResponse sendInitMessage := func() { + l.log.Debug(ctx, "sending dc init message", slog.F("msg", init)) initData, err := json.Marshal(&init) if err != nil { + l.log.Debug(ctx, "failed to marshal dc init message", slog.Error(err)) rw.Close() return } _, err = rw.Write(initData) if err != nil { + l.log.Debug(ctx, "failed to write dc init message", slog.Error(err)) return } if init.Err != "" { // If an error occurred, we're safe to close the connection. + l.log.Debug(ctx, "closing data channel due to error", slog.F("msg", init.Err)) dc.Close() return } @@ -332,8 +396,10 @@ func (l *listener) handle(msg BrokerMessage) func(dc *webrtc.DataChannel) { return } + l.log.Debug(ctx, "dialing remote address", slog.F("network", network), slog.F("addr", addr)) nc, err := net.Dial(network, addr) if err != nil { + l.log.Debug(ctx, "failed to dial remote address") init.Code = CodeDialErr init.Err = err.Error() if op, ok := err.(*net.OpError); ok { @@ -345,8 +411,10 @@ func (l *listener) handle(msg BrokerMessage) func(dc *webrtc.DataChannel) { if init.Err != "" { return } + // Must wrap the data channel inside this connection // for buffering from the dialed endpoint to the client. + l.log.Debug(ctx, "data channel initialized, tunnelling") co := &dataChannelConn{ addr: nil, dc: dc, @@ -366,13 +434,25 @@ func (l *listener) handle(msg BrokerMessage) func(dc *webrtc.DataChannel) { // Close closes the broker socket and all created RTC connections. func (l *listener) Close() error { + l.log.Info(context.Background(), "listener closed") + l.connClosersMut.Lock() + defer l.connClosersMut.Unlock() + + select { + case _, ok := <-l.closed: + if !ok { + return errors.New("already closed") + } + default: + } + close(l.closed) + for _, connCloser := range l.connClosers { // We can ignore the error here... it doesn't // really matter if these fail to close. _ = connCloser.Close() } - l.connClosersMut.Unlock() return l.ws.Close(websocket.StatusNormalClosure, "") } diff --git a/wsnet/listen_test.go b/wsnet/listen_test.go index 2c5ba35f..78b56691 100644 --- a/wsnet/listen_test.go +++ b/wsnet/listen_test.go @@ -2,12 +2,13 @@ package wsnet import ( "context" - "fmt" - "net" "net/http" + "net/http/httptest" "testing" "time" + "cdr.dev/slog/sloggers/slogtest" + "github.com/stretchr/testify/require" "nhooyr.io/websocket" ) @@ -21,9 +22,6 @@ func TestListen(t *testing.T) { var ( connCh = make(chan *websocket.Conn) mux = http.NewServeMux() - srv = http.Server{ - Handler: mux, - } ) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { ws, err := websocket.Accept(w, r, nil) @@ -34,39 +32,20 @@ func TestListen(t *testing.T) { connCh <- ws }) - listener, err := net.Listen("tcp4", "127.0.0.1:0") - if err != nil { - t.Error(err) - return - } - go func() { - _ = srv.Serve(listener) - }() - addr := listener.Addr() - broker := fmt.Sprintf("http://%s/", addr.String()) + s := httptest.NewServer(mux) + defer s.Close() - _, err = Listen(context.Background(), broker, "") - if err != nil { - t.Error(err) - return - } + l, err := Listen(context.Background(), slogtest.Make(t, nil), s.URL, "") + require.NoError(t, err) + defer l.Close() conn := <-connCh - _ = listener.Close() - // We need to close the connection too... closing a TCP - // listener does not close active local connections. - _ = conn.Close(websocket.StatusGoingAway, "") + + // Kill the server connection. + err = conn.Close(websocket.StatusGoingAway, "") + require.NoError(t, err) // At least a few retry attempts should be had... time.Sleep(connectionRetryInterval * 5) - - listener, err = net.Listen("tcp4", addr.String()) - if err != nil { - t.Error(err) - return - } - go func() { - _ = srv.Serve(listener) - }() <-connCh }) } From f3378973e65ea7ee61570d40d5f39aed3375fa8d Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 20 Jul 2021 17:12:10 -0600 Subject: [PATCH 067/128] feat: Add ActiveConnections function to track usage (#390) * feat: Add ActiveConnections function tto track usage * Fix linting errors * Fix syntax err --- wsnet/dial.go | 11 +++++++++++ wsnet/dial_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/wsnet/dial.go b/wsnet/dial.go index 97d49827..050bc574 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -232,6 +232,17 @@ func (d *Dialer) Closed() <-chan struct{} { return d.closedChan } +// ActiveConnections returns the amount of active connections. +// DialContext opens a connection, and close will end it. +func (d *Dialer) ActiveConnections() int { + stats, ok := d.rtc.GetStats().GetConnectionStats(d.rtc) + if !ok { + return -1 + } + // Subtract 1 for the control channel. + return int(stats.DataChannelsRequested-stats.DataChannelsClosed) - 1 +} + // Close closes the RTC connection. // All data channels dialed will be closed. func (d *Dialer) Close() error { diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index fcd00ac7..a5d33b96 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -243,6 +243,38 @@ func TestDial(t *testing.T) { t.Error("didn't close in time") } }) + + t.Run("Active Connections", func(t *testing.T) { + t.Parallel() + + listener, err := net.Listen("tcp", "0.0.0.0:0") + if err != nil { + t.Error(err) + return + } + go func() { + _, _ = listener.Accept() + }() + connectAddr, listenAddr := createDumbBroker(t) + _, err = Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + if err != nil { + t.Error(err) + return + } + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + if err != nil { + t.Error(err) + } + conn, _ := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) + assert.Equal(t, 1, dialer.ActiveConnections()) + _ = conn.Close() + assert.Equal(t, 0, dialer.ActiveConnections()) + _, _ = dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) + conn, _ = dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) + assert.Equal(t, 2, dialer.ActiveConnections()) + _ = conn.Close() + assert.Equal(t, 1, dialer.ActiveConnections()) + }) } func BenchmarkThroughput(b *testing.B) { From 08fe80053bae1f37d3c3ee1496f6fb36cd44ef1f Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 20 Jul 2021 19:59:10 -0600 Subject: [PATCH 068/128] feat: Add DialCache for key-based connection caching (#391) * feat: Add DialCache for key-based connection caching * Remove DialOptions * Move DialFunc to Dial * Add WS options to dial * Requested changes * Add comment * Fixup --- internal/cmd/tunnel.go | 1 + wsnet/cache.go | 171 +++++++++++++++++++++++++++++++++++++++++ wsnet/cache_test.go | 71 +++++++++++++++++ wsnet/dial.go | 12 ++- wsnet/dial_test.go | 22 +++--- 5 files changed, 263 insertions(+), 14 deletions(-) create mode 100644 wsnet/cache.go create mode 100644 wsnet/cache_test.go diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index 9c12dd37..956e9fd2 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -112,6 +112,7 @@ func (c *tunnneler) start(ctx context.Context) error { TURNProxyURL: c.brokerAddr, ICEServers: []webrtc.ICEServer{wsnet.TURNProxyICECandidate()}, }, + nil, ) if err != nil { return xerrors.Errorf("creating workspace dialer: %w", err) diff --git a/wsnet/cache.go b/wsnet/cache.go new file mode 100644 index 00000000..e62aa0a9 --- /dev/null +++ b/wsnet/cache.go @@ -0,0 +1,171 @@ +package wsnet + +import ( + "context" + "errors" + "sync" + "time" + + "golang.org/x/sync/singleflight" +) + +// DialCache constructs a new DialerCache. +// The cache clears connections that: +// 1. Are older than the TTL and have no active user-created connections. +// 2. Have been closed. +func DialCache(ttl time.Duration) *DialerCache { + dc := &DialerCache{ + ttl: ttl, + closed: make(chan struct{}), + flightGroup: &singleflight.Group{}, + mut: sync.RWMutex{}, + dialers: make(map[string]*Dialer), + atime: make(map[string]time.Time), + } + go dc.init() + return dc +} + +type DialerCache struct { + ttl time.Duration + flightGroup *singleflight.Group + closed chan struct{} + mut sync.RWMutex + + // Key is the "key" of a dialer, which is usually the workspace ID. + dialers map[string]*Dialer + atime map[string]time.Time +} + +// init starts the ticker for evicting connections. +func (d *DialerCache) init() { + ticker := time.NewTicker(time.Second * 30) + defer ticker.Stop() + for { + select { + case <-d.closed: + return + case <-ticker.C: + d.evict() + } + } +} + +// evict removes lost/broken/expired connections from the cache. +func (d *DialerCache) evict() { + var wg sync.WaitGroup + d.mut.RLock() + for key, dialer := range d.dialers { + wg.Add(1) + key := key + dialer := dialer + go func() { + defer wg.Done() + + evict := false + select { + case <-dialer.Closed(): + evict = true + default: + } + if dialer.ActiveConnections() == 0 && time.Since(d.atime[key]) >= d.ttl { + evict = true + } + // If we're already evicting there's no point in trying to ping. + if !evict { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) + defer cancel() + err := dialer.Ping(ctx) + if err != nil { + evict = true + } + } + + if !evict { + return + } + + _ = dialer.Close() + // Ensure after Ping and potential delays that we're still testing against + // the proper dialer. + if dialer != d.dialers[key] { + return + } + + d.mut.Lock() + defer d.mut.Unlock() + delete(d.atime, key) + delete(d.dialers, key) + }() + } + d.mut.RUnlock() + wg.Wait() +} + +// Dial returns a Dialer from the cache if one exists with the key provided, +// or dials a new connection using the dialerFunc. +// The bool returns whether the connection was found in the cache or not. +func (d *DialerCache) Dial(ctx context.Context, key string, dialerFunc func() (*Dialer, error)) (*Dialer, bool, error) { + select { + case <-d.closed: + return nil, false, errors.New("cache closed") + default: + } + + d.mut.RLock() + dialer, ok := d.dialers[key] + d.mut.RUnlock() + if ok { + closed := false + select { + case <-dialer.Closed(): + closed = true + default: + } + if !closed { + d.mut.Lock() + d.atime[key] = time.Now() + d.mut.Unlock() + + return dialer, true, nil + } + } + + rawDialer, err, _ := d.flightGroup.Do(key, func() (interface{}, error) { + dialer, err := dialerFunc() + if err != nil { + return nil, err + } + d.mut.Lock() + d.dialers[key] = dialer + d.atime[key] = time.Now() + d.mut.Unlock() + + return dialer, nil + }) + if err != nil { + return nil, false, err + } + select { + case <-d.closed: + return nil, false, errors.New("cache closed") + default: + } + + return rawDialer.(*Dialer), false, nil +} + +// Close closes all cached dialers. +func (d *DialerCache) Close() error { + d.mut.Lock() + defer d.mut.Unlock() + + for _, dialer := range d.dialers { + err := dialer.Close() + if err != nil { + return err + } + } + close(d.closed) + return nil +} diff --git a/wsnet/cache_test.go b/wsnet/cache_test.go new file mode 100644 index 00000000..44edb608 --- /dev/null +++ b/wsnet/cache_test.go @@ -0,0 +1,71 @@ +package wsnet + +import ( + "context" + "testing" + "time" + + "cdr.dev/slog/sloggers/slogtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCache(t *testing.T) { + dialFunc := func(connectAddr string) func() (*Dialer, error) { + return func() (*Dialer, error) { + return DialWebsocket(context.Background(), connectAddr, nil, nil) + } + } + + t.Run("Caches", func(t *testing.T) { + connectAddr, listenAddr := createDumbBroker(t) + l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + require.NoError(t, err) + defer l.Close() + + cache := DialCache(time.Hour) + c1, cached, err := cache.Dial(context.Background(), "example", dialFunc(connectAddr)) + require.NoError(t, err) + require.Equal(t, cached, false) + c2, cached, err := cache.Dial(context.Background(), "example", dialFunc(connectAddr)) + require.NoError(t, err) + require.Equal(t, cached, true) + assert.Same(t, c1, c2) + }) + + t.Run("Create If Closed", func(t *testing.T) { + connectAddr, listenAddr := createDumbBroker(t) + l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + require.NoError(t, err) + defer l.Close() + + cache := DialCache(time.Hour) + + c1, cached, err := cache.Dial(context.Background(), "example", dialFunc(connectAddr)) + require.NoError(t, err) + require.Equal(t, cached, false) + require.NoError(t, c1.Close()) + c2, cached, err := cache.Dial(context.Background(), "example", dialFunc(connectAddr)) + require.NoError(t, err) + require.Equal(t, cached, false) + assert.NotSame(t, c1, c2) + }) + + t.Run("Evict No Connections", func(t *testing.T) { + connectAddr, listenAddr := createDumbBroker(t) + l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + require.NoError(t, err) + defer l.Close() + + cache := DialCache(0) + + c1, cached, err := cache.Dial(context.Background(), "example", dialFunc(connectAddr)) + require.NoError(t, err) + require.Equal(t, cached, false) + cache.evict() + c2, cached, err := cache.Dial(context.Background(), "example", dialFunc(connectAddr)) + require.NoError(t, err) + require.Equal(t, cached, false) + assert.NotSame(t, c1, c2) + }) +} diff --git a/wsnet/dial.go b/wsnet/dial.go index 050bc574..af4b422c 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -35,8 +35,8 @@ type DialOptions struct { } // DialWebsocket dials the broker with a WebSocket and negotiates a connection. -func DialWebsocket(ctx context.Context, broker string, options *DialOptions) (*Dialer, error) { - conn, resp, err := websocket.Dial(ctx, broker, nil) +func DialWebsocket(ctx context.Context, broker string, netOpts *DialOptions, wsOpts *websocket.DialOptions) (*Dialer, error) { + conn, resp, err := websocket.Dial(ctx, broker, wsOpts) if err != nil { if resp != nil { defer func() { @@ -52,7 +52,7 @@ func DialWebsocket(ctx context.Context, broker string, options *DialOptions) (*D // We should close the socket intentionally. _ = conn.Close(websocket.StatusInternalError, "an error occurred") }() - return Dial(nconn, options) + return Dial(nconn, netOpts) } // Dial negotiates a connection to a listener. @@ -246,6 +246,12 @@ func (d *Dialer) ActiveConnections() int { // Close closes the RTC connection. // All data channels dialed will be closed. func (d *Dialer) Close() error { + select { + case <-d.closedChan: + return nil + default: + } + close(d.closedChan) return d.rtc.Close() } diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index a5d33b96..8a6486ba 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -39,7 +39,7 @@ func ExampleDial_basic() { dialer, err := DialWebsocket(context.Background(), "wss://master.cdr.dev/agent/workspace/connect", &DialOptions{ ICEServers: servers, - }) + }, nil) if err != nil { // Do something... } @@ -60,7 +60,7 @@ func TestDial(t *testing.T) { require.NoError(t, err) defer l.Close() - dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) require.NoError(t, err) err = dialer.Ping(context.Background()) @@ -83,7 +83,7 @@ func TestDial(t *testing.T) { Credential: testPass, CredentialType: webrtc.ICECredentialTypePassword, }}, - }) + }, nil) require.NoError(t, err) _ = dialer.Ping(context.Background()) @@ -100,7 +100,7 @@ func TestDial(t *testing.T) { require.NoError(t, err) defer l.Close() - dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) require.NoError(t, err) _, err = dialer.DialContext(context.Background(), "tcp", "localhost:100") @@ -130,7 +130,7 @@ func TestDial(t *testing.T) { require.NoError(t, err) defer l.Close() - dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) require.NoError(t, err) conn, err := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) @@ -158,7 +158,7 @@ func TestDial(t *testing.T) { require.NoError(t, err) defer l.Close() - dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) require.NoError(t, err) conn, err := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) @@ -178,7 +178,7 @@ func TestDial(t *testing.T) { require.NoError(t, err) defer l.Close() - dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) require.NoError(t, err) err = dialer.Close() @@ -210,7 +210,7 @@ func TestDial(t *testing.T) { Credential: testPass, CredentialType: webrtc.ICECredentialTypePassword, }}, - }) + }, nil) require.NoError(t, err) conn, err := dialer.DialContext(context.Background(), "tcp", tcpListener.Addr().String()) @@ -231,7 +231,7 @@ func TestDial(t *testing.T) { require.NoError(t, err) defer l.Close() - dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) require.NoError(t, err) go func() { _ = dialer.Close() @@ -261,7 +261,7 @@ func TestDial(t *testing.T) { t.Error(err) return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) if err != nil { t.Error(err) } @@ -314,7 +314,7 @@ func BenchmarkThroughput(b *testing.B) { } defer l.Close() - dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) if err != nil { b.Error(err) return From 7f0e87d61f66456a21ec6b960f0173829f57be21 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 21 Jul 2021 10:03:11 -0600 Subject: [PATCH 069/128] fix: Increase ICE timeouts (#392) * fix: Increase ICE timeouts * Fix ping times --- wsnet/dial.go | 27 ++++++++++++++++++++++----- wsnet/rtc.go | 4 +++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/wsnet/dial.go b/wsnet/dial.go index af4b422c..283bedf4 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -137,6 +137,7 @@ type Dialer struct { closedChan chan struct{} connClosers []io.Closer connClosersMut sync.Mutex + pingMut sync.Mutex } func (d *Dialer) negotiate() (err error) { @@ -160,7 +161,7 @@ func (d *Dialer) negotiate() (err error) { return } d.rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { - if pcs == webrtc.PeerConnectionStateConnected { + if pcs != webrtc.PeerConnectionStateDisconnected { return } @@ -178,6 +179,7 @@ func (d *Dialer) negotiate() (err error) { default: } close(d.closedChan) + _ = d.rtc.Close() }) }() @@ -263,7 +265,7 @@ func (d *Dialer) Ping(ctx context.Context) error { // Since we control the client and server we could open this // data channel with `Negotiated` true to reduce traffic being // sent when the RTC connection is opened. - err := waitForDataChannelOpen(context.Background(), d.ctrl) + err := waitForDataChannelOpen(ctx, d.ctrl) if err != nil { return err } @@ -273,13 +275,28 @@ func (d *Dialer) Ping(ctx context.Context) error { return err } } + d.pingMut.Lock() + defer d.pingMut.Unlock() _, err = d.ctrlrw.Write([]byte{'a'}) if err != nil { return fmt.Errorf("write: %w", err) } - b := make([]byte, 4) - _, err = d.ctrlrw.Read(b) - return err + errCh := make(chan error) + go func() { + // There's a race in which connections can get lost-mid ping + // in which case this would block forever. + defer close(errCh) + _, err = d.ctrlrw.Read(make([]byte, 4)) + errCh <- err + }() + ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) + defer cancelFunc() + select { + case err := <-errCh: + return err + case <-ctx.Done(): + return ctx.Err() + } } // DialContext dials the network and address on the remote listener. diff --git a/wsnet/rtc.go b/wsnet/rtc.go index 05c04f1b..0b93c57c 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -160,7 +160,9 @@ func newPeerConnection(servers []webrtc.ICEServer, dialer proxy.Dialer) (*webrtc se.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeUDP4}) se.SetSrflxAcceptanceMinWait(0) se.DetachDataChannels() - se.SetICETimeouts(time.Second*3, time.Second*3, time.Second*2) + // If the disconnect and keep-alive timeouts are too closely related, we'll + // experience "random" connection failures. + se.SetICETimeouts(time.Second*5, time.Second*25, time.Second*2) lf := logging.NewDefaultLoggerFactory() lf.DefaultLogLevel = logging.LogLevelDisabled se.LoggerFactory = lf From b1d9ef4b9129c689d889c0b86f60cd5023927cff Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 21 Jul 2021 13:45:38 -0600 Subject: [PATCH 070/128] chore: Remove Closed chan in favor of reconnecting DataChannels (#393) * chore: Remove Closed chan in favor of reconnecting DataChannels * Evict quickly * Fix closed connection caching * Fix HTTP check * Switch on HTTP(s) --- wsnet/cache.go | 32 +++++++++++--------------------- wsnet/conn.go | 9 +++++++-- wsnet/dial.go | 28 +++------------------------- wsnet/dial_test.go | 30 ++++-------------------------- 4 files changed, 25 insertions(+), 74 deletions(-) diff --git a/wsnet/cache.go b/wsnet/cache.go index e62aa0a9..b16950ca 100644 --- a/wsnet/cache.go +++ b/wsnet/cache.go @@ -6,6 +6,7 @@ import ( "sync" "time" + "github.com/pion/webrtc/v3" "golang.org/x/sync/singleflight" ) @@ -39,7 +40,7 @@ type DialerCache struct { // init starts the ticker for evicting connections. func (d *DialerCache) init() { - ticker := time.NewTicker(time.Second * 30) + ticker := time.NewTicker(time.Second * 5) defer ticker.Stop() for { select { @@ -62,17 +63,11 @@ func (d *DialerCache) evict() { go func() { defer wg.Done() - evict := false - select { - case <-dialer.Closed(): + // If we're no longer signaling, the connection is pending close. + evict := dialer.rtc.SignalingState() == webrtc.SignalingStateClosed + if dialer.activeConnections() == 0 && time.Since(d.atime[key]) >= d.ttl { evict = true - default: - } - if dialer.ActiveConnections() == 0 && time.Since(d.atime[key]) >= d.ttl { - evict = true - } - // If we're already evicting there's no point in trying to ping. - if !evict { + } else { ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) defer cancel() err := dialer.Ping(ctx) @@ -116,17 +111,12 @@ func (d *DialerCache) Dial(ctx context.Context, key string, dialerFunc func() (* dialer, ok := d.dialers[key] d.mut.RUnlock() if ok { - closed := false - select { - case <-dialer.Closed(): - closed = true - default: - } - if !closed { - d.mut.Lock() - d.atime[key] = time.Now() - d.mut.Unlock() + d.mut.Lock() + d.atime[key] = time.Now() + d.mut.Unlock() + // The connection is pending close here... + if dialer.rtc.SignalingState() != webrtc.SignalingStateClosed { return dialer, true, nil } } diff --git a/wsnet/conn.go b/wsnet/conn.go index 5b863f04..de67c3c4 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -2,6 +2,7 @@ package wsnet import ( "context" + "errors" "fmt" "net" "net/http" @@ -73,9 +74,13 @@ func (t *turnProxyDialer) Dial(network, addr string) (c net.Conn, err error) { // Copy the baseURL so we can adjust path. url := *t.baseURL - url.Scheme = "wss" - if url.Scheme == httpScheme { + switch url.Scheme { + case "http": url.Scheme = "ws" + case "https": + url.Scheme = "wss" + default: + return nil, errors.New("invalid turn url addr scheme provided") } url.Path = "/api/private/turn" conn, resp, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{ diff --git a/wsnet/dial.go b/wsnet/dial.go index 283bedf4..394e6a9f 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -118,7 +118,6 @@ func Dial(conn net.Conn, options *DialOptions) (*Dialer, error) { conn: conn, ctrl: ctrl, rtc: rtc, - closedChan: make(chan struct{}), connClosers: []io.Closer{ctrl}, } @@ -134,7 +133,6 @@ type Dialer struct { ctrlrw datachannel.ReadWriteCloser rtc *webrtc.PeerConnection - closedChan chan struct{} connClosers []io.Closer connClosersMut sync.Mutex pingMut sync.Mutex @@ -161,25 +159,17 @@ func (d *Dialer) negotiate() (err error) { return } d.rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { - if pcs != webrtc.PeerConnectionStateDisconnected { + if pcs == webrtc.PeerConnectionStateConnected { return } - // Close connections opened while the RTC was alive. + // Close connections opened when RTC was alive. d.connClosersMut.Lock() defer d.connClosersMut.Unlock() for _, connCloser := range d.connClosers { _ = connCloser.Close() } d.connClosers = make([]io.Closer, 0) - - select { - case <-d.closedChan: - return - default: - } - close(d.closedChan) - _ = d.rtc.Close() }) }() @@ -228,15 +218,9 @@ func (d *Dialer) negotiate() (err error) { return <-errCh } -// Closed returns a channel that closes when -// the connection is closed. -func (d *Dialer) Closed() <-chan struct{} { - return d.closedChan -} - // ActiveConnections returns the amount of active connections. // DialContext opens a connection, and close will end it. -func (d *Dialer) ActiveConnections() int { +func (d *Dialer) activeConnections() int { stats, ok := d.rtc.GetStats().GetConnectionStats(d.rtc) if !ok { return -1 @@ -248,12 +232,6 @@ func (d *Dialer) ActiveConnections() int { // Close closes the RTC connection. // All data channels dialed will be closed. func (d *Dialer) Close() error { - select { - case <-d.closedChan: - return nil - default: - } - close(d.closedChan) return d.rtc.Close() } diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 8a6486ba..5dd11b58 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -9,7 +9,6 @@ import ( "net" "strconv" "testing" - "time" "cdr.dev/slog/sloggers/slogtest" "github.com/pion/ice/v2" @@ -223,27 +222,6 @@ func TestDial(t *testing.T) { assert.ErrorIs(t, err, io.EOF) }) - t.Run("Closed", func(t *testing.T) { - t.Parallel() - - connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") - require.NoError(t, err) - defer l.Close() - - dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) - require.NoError(t, err) - go func() { - _ = dialer.Close() - }() - - select { - case <-dialer.Closed(): - case <-time.NewTimer(time.Second).C: - t.Error("didn't close in time") - } - }) - t.Run("Active Connections", func(t *testing.T) { t.Parallel() @@ -266,14 +244,14 @@ func TestDial(t *testing.T) { t.Error(err) } conn, _ := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) - assert.Equal(t, 1, dialer.ActiveConnections()) + assert.Equal(t, 1, dialer.activeConnections()) _ = conn.Close() - assert.Equal(t, 0, dialer.ActiveConnections()) + assert.Equal(t, 0, dialer.activeConnections()) _, _ = dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) conn, _ = dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) - assert.Equal(t, 2, dialer.ActiveConnections()) + assert.Equal(t, 2, dialer.activeConnections()) _ = conn.Close() - assert.Equal(t, 1, dialer.ActiveConnections()) + assert.Equal(t, 1, dialer.activeConnections()) }) } From 3e7872942b251043098198b80053e6bd54d0033a Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 21 Jul 2021 14:51:03 -0600 Subject: [PATCH 071/128] fix: Add context to negotiate RTC func (#394) * fix: Add context to negotiate RTC func * Wait for connection open with proper context * Add deadline if doesn't exist * Defer multiple cancels * Update wsnet/rtc.go Co-authored-by: Dean Sheather * Fix var name Co-authored-by: Dean Sheather --- wsnet/dial.go | 10 +++++----- wsnet/rtc.go | 11 ++++++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/wsnet/dial.go b/wsnet/dial.go index 394e6a9f..d6f45461 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -52,11 +52,11 @@ func DialWebsocket(ctx context.Context, broker string, netOpts *DialOptions, wsO // We should close the socket intentionally. _ = conn.Close(websocket.StatusInternalError, "an error occurred") }() - return Dial(nconn, netOpts) + return Dial(ctx, nconn, netOpts) } // Dial negotiates a connection to a listener. -func Dial(conn net.Conn, options *DialOptions) (*Dialer, error) { +func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, error) { if options == nil { options = &DialOptions{} } @@ -121,7 +121,7 @@ func Dial(conn net.Conn, options *DialOptions) (*Dialer, error) { connClosers: []io.Closer{ctrl}, } - return dialer, dialer.negotiate() + return dialer, dialer.negotiate(ctx) } // Dialer enables arbitrary dialing to any network and address @@ -138,7 +138,7 @@ type Dialer struct { pingMut sync.Mutex } -func (d *Dialer) negotiate() (err error) { +func (d *Dialer) negotiate(ctx context.Context) (err error) { var ( decoder = json.NewDecoder(d.conn) errCh = make(chan error) @@ -153,7 +153,7 @@ func (d *Dialer) negotiate() (err error) { defer func() { _ = d.conn.Close() }() - err := waitForConnectionOpen(context.Background(), d.rtc) + err := waitForConnectionOpen(ctx, d.rtc) if err != nil { errCh <- err return diff --git a/wsnet/rtc.go b/wsnet/rtc.go index 0b93c57c..79702743 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -242,11 +242,16 @@ func waitForConnectionOpen(ctx context.Context, conn *webrtc.PeerConnection) err if conn.ConnectionState() == webrtc.PeerConnectionStateConnected { return nil } - ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) - defer cancelFunc() + var cancel context.CancelFunc + if _, deadlineSet := ctx.Deadline(); deadlineSet { + ctx, cancel = context.WithCancel(ctx) + } else { + ctx, cancel = context.WithTimeout(ctx, time.Second*15) + } + defer cancel() conn.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { if pcs == webrtc.PeerConnectionStateConnected { - cancelFunc() + cancel() } }) <-ctx.Done() From 53e7c3312adeba59c02681b8d22617940f4c5fd8 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 21 Jul 2021 18:33:37 -0600 Subject: [PATCH 072/128] fix: Close conn if context deadline is exceeded (#395) * fix: Close conn if context deadline is exceeded Previously the error occurred but never propogated due to blocking fromm the read loop in negotiate. Now, connection timeouts can properly occur. * Fixes --- wsnet/dial.go | 4 +++- wsnet/dial_test.go | 13 +++++++++++++ wsnet/rtc.go | 2 +- wsnet/wsnet_test.go | 4 +++- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/wsnet/dial.go b/wsnet/dial.go index d6f45461..3880b12c 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -147,7 +147,6 @@ func (d *Dialer) negotiate(ctx context.Context) (err error) { // so it's better to buffer and process than fail. pendingCandidates = []webrtc.ICECandidateInit{} ) - go func() { defer close(errCh) defer func() { @@ -155,6 +154,9 @@ func (d *Dialer) negotiate(ctx context.Context) (err error) { }() err := waitForConnectionOpen(ctx, d.rtc) if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + _ = d.conn.Close() + } errCh <- err return } diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 5dd11b58..5d75cfd4 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -9,6 +9,7 @@ import ( "net" "strconv" "testing" + "time" "cdr.dev/slog/sloggers/slogtest" "github.com/pion/ice/v2" @@ -51,6 +52,18 @@ func ExampleDial_basic() { } func TestDial(t *testing.T) { + t.Run("Timeout", func(t *testing.T) { + t.Parallel() + + connectAddr, _ := createDumbBroker(t) + + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Millisecond*50) + defer cancelFunc() + dialer, err := DialWebsocket(ctx, connectAddr, nil, nil) + require.True(t, errors.Is(err, context.DeadlineExceeded)) + require.Error(t, dialer.conn.Close(), "already wrote close") + }) + t.Run("Ping", func(t *testing.T) { t.Parallel() diff --git a/wsnet/rtc.go b/wsnet/rtc.go index 79702743..32a089a2 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -256,7 +256,7 @@ func waitForConnectionOpen(ctx context.Context, conn *webrtc.PeerConnection) err }) <-ctx.Done() if ctx.Err() == context.DeadlineExceeded { - return ctx.Err() + return context.DeadlineExceeded } return nil } diff --git a/wsnet/wsnet_test.go b/wsnet/wsnet_test.go index ad9ac381..20aa7699 100644 --- a/wsnet/wsnet_test.go +++ b/wsnet/wsnet_test.go @@ -68,7 +68,9 @@ func createDumbBroker(t testing.TB) (connectAddr string, listenAddr string) { mut.Lock() defer mut.Unlock() if sess == nil { - t.Error("listen not called") + // We discard inbound to emulate a pubsub where we don't know if anyone + // is listening on the other side. + _, _ = io.Copy(io.Discard, nc) return } oc, err := sess.Open() From 485026652de6fd5be957a1188883682d606285b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jul 2021 13:46:49 -0600 Subject: [PATCH 073/128] chore: bump github.com/pion/ice/v2 from 2.1.8 to 2.1.9 (#396) Bumps [github.com/pion/ice/v2](https://github.com/pion/ice) from 2.1.8 to 2.1.9. - [Release notes](https://github.com/pion/ice/releases) - [Commits](https://github.com/pion/ice/compare/v2.1.8...v2.1.9) --- updated-dependencies: - dependency-name: github.com/pion/ice/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 33895cc2..583311bf 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/pion/datachannel v1.4.21 github.com/pion/dtls/v2 v2.0.9 - github.com/pion/ice/v2 v2.1.8 + github.com/pion/ice/v2 v2.1.9 github.com/pion/logging v0.2.2 github.com/pion/turn/v2 v2.0.5 github.com/pion/webrtc/v3 v3.0.31 diff --git a/go.sum b/go.sum index 00a35ce5..e6bda4ba 100644 --- a/go.sum +++ b/go.sum @@ -314,8 +314,9 @@ github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXm github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= github.com/pion/dtls/v2 v2.0.9 h1:7Ow+V++YSZQMYzggI0P9vLJz/hUFcffsfGMfT/Qy+u8= github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQKYQho= -github.com/pion/ice/v2 v2.1.8 h1:3kV4XaB2C3z1gDUXZmwSB/B0PSdZ7GFFC3w4iUX9prs= github.com/pion/ice/v2 v2.1.8/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= +github.com/pion/ice/v2 v2.1.9 h1:vl1X0PaR3qJjxuL6fsvsPlT9UOb7TCqEmHL9tMXJJMA= +github.com/pion/ice/v2 v2.1.9/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= github.com/pion/interceptor v0.0.13 h1:fnV+b0p/KEzwwr/9z2nsSqA9IQRMsM4nF5HjrNSWwBo= github.com/pion/interceptor v0.0.13/go.mod h1:svsW2QoLHLoGLUr4pDoSopGBEWk8FZwlfxId/OKRKzo= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= From 3e699aad0edaf4d89c91d8070253366a4fee18cc Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 26 Jul 2021 10:18:38 -0600 Subject: [PATCH 074/128] fix: TCP connections leaking after RTC disconnects (#397) * fix: TCP connections leaking after RTC disconnects * Fix race condition * Add switch * Lower tick time * Fix data race * Update Go version * Fix log err * Fix race --- ci/image/Dockerfile | 2 +- wsnet/conn.go | 2 ++ wsnet/dial_test.go | 37 +++++++++++++++++++++++++++++++++++++ wsnet/listen.go | 31 ++++++++++++++++++++++--------- 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index 08dde84b..b77bf8aa 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.16.3 +FROM golang:1.16.5 ENV GOFLAGS="-mod=readonly" ENV CI=true diff --git a/wsnet/conn.go b/wsnet/conn.go index de67c3c4..40fa50ae 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -129,6 +129,8 @@ type dataChannelConn struct { } func (c *dataChannelConn) init() { + c.closedMutex.Lock() + defer c.closedMutex.Unlock() c.sendMore = make(chan struct{}, 1) c.dc.SetBufferedAmountLowThreshold(bufferedAmountLowThreshold) c.dc.OnBufferedAmountLow(func() { diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 5d75cfd4..79e7ae39 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -266,6 +266,43 @@ func TestDial(t *testing.T) { _ = conn.Close() assert.Equal(t, 1, dialer.activeConnections()) }) + + t.Run("Close Listeners on Disconnect", func(t *testing.T) { + t.Parallel() + + tcpListener, err := net.Listen("tcp", "0.0.0.0:0") + require.NoError(t, err) + go func() { + _, _ = tcpListener.Accept() + }() + + connectAddr, listenAddr := createDumbBroker(t) + l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + require.NoError(t, err) + + turnAddr, closeTurn := createTURNServer(t, ice.SchemeTypeTURN) + dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ + ICEServers: []webrtc.ICEServer{{ + URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, + Username: "example", + Credential: testPass, + CredentialType: webrtc.ICECredentialTypePassword, + }}, + }, nil) + require.NoError(t, err) + + _, err = dialer.DialContext(context.Background(), "tcp", tcpListener.Addr().String()) + require.NoError(t, err) + + closeTurn() + + list := l.(*listener) + assert.Eventually(t, func() bool { + list.connClosersMut.Lock() + defer list.connClosersMut.Unlock() + return len(list.connClosers) == 0 + }, time.Second*15, time.Millisecond*100) + }) } func BenchmarkThroughput(b *testing.B) { diff --git a/wsnet/listen.go b/wsnet/listen.go index 02f13f41..803e140b 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -159,9 +159,11 @@ func (l *listener) dial(ctx context.Context) (<-chan error, error) { // so the cognitive overload linter has been disabled. // nolint:gocognit,nestif func (l *listener) negotiate(ctx context.Context, conn net.Conn) { + id := atomic.AddInt64(&l.nextConnNumber, 1) + ctx = slog.With(ctx, slog.F("conn_id", id)) + var ( err error - id = atomic.AddInt64(&l.nextConnNumber, 1) decoder = json.NewDecoder(conn) rtc *webrtc.PeerConnection // If candidates are sent before an offer, we place them here. @@ -171,7 +173,7 @@ func (l *listener) negotiate(ctx context.Context, conn net.Conn) { // Sends the error provided then closes the connection. // If RTC isn't connected, we'll close it. closeError = func(err error) { - l.log.Warn(ctx, "negotiation error, closing connection", slog.Error(err)) + // l.log.Warn(ctx, "negotiation error, closing connection", slog.Error(err)) d, _ := json.Marshal(&BrokerMessage{ Error: err.Error(), @@ -187,7 +189,6 @@ func (l *listener) negotiate(ctx context.Context, conn net.Conn) { } ) - ctx = slog.With(ctx, slog.F("conn_id", id)) l.log.Info(ctx, "accepted new session from broker connection, negotiating") for { @@ -255,17 +256,26 @@ func (l *listener) negotiate(ctx context.Context, conn net.Conn) { return } rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { - l.log.Debug(ctx, "connection state change", slog.F("state", pcs.String())) - if pcs == webrtc.PeerConnectionStateConnecting { + l.log.Info(ctx, "connection state change", slog.F("state", pcs.String())) + switch pcs { + case webrtc.PeerConnectionStateConnected: return + case webrtc.PeerConnectionStateConnecting: + // Safe to close the negotiating WebSocket. + _ = conn.Close() + return + } + + // Close connections opened when RTC was alive. + l.connClosersMut.Lock() + defer l.connClosersMut.Unlock() + for _, connCloser := range l.connClosers { + _ = connCloser.Close() } - _ = conn.Close() + l.connClosers = make([]io.Closer, 0) }) flushCandidates := proxyICECandidates(rtc, conn) - l.connClosersMut.Lock() - l.connClosers = append(l.connClosers, rtc) - l.connClosersMut.Unlock() rtc.OnDataChannel(l.handle(ctx, msg)) l.log.Debug(ctx, "set remote description", slog.F("offer", *msg.Offer)) @@ -420,6 +430,9 @@ func (l *listener) handle(ctx context.Context, msg BrokerMessage) func(dc *webrt dc: dc, rw: rw, } + l.connClosersMut.Lock() + l.connClosers = append(l.connClosers, co) + l.connClosersMut.Unlock() co.init() defer nc.Close() defer co.Close() From 17bab569e5561a0cab50f1b864393f5ab4fb308a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jul 2021 10:19:17 -0600 Subject: [PATCH 075/128] chore: bump github.com/pion/webrtc/v3 from 3.0.31 to 3.0.32 (#399) Bumps [github.com/pion/webrtc/v3](https://github.com/pion/webrtc) from 3.0.31 to 3.0.32. - [Release notes](https://github.com/pion/webrtc/releases) - [Commits](https://github.com/pion/webrtc/compare/v3.0.31...v3.0.32) --- updated-dependencies: - dependency-name: github.com/pion/webrtc/v3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 583311bf..2539ea82 100644 --- a/go.mod +++ b/go.mod @@ -16,10 +16,10 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/pion/datachannel v1.4.21 github.com/pion/dtls/v2 v2.0.9 - github.com/pion/ice/v2 v2.1.9 + github.com/pion/ice/v2 v2.1.10 github.com/pion/logging v0.2.2 github.com/pion/turn/v2 v2.0.5 - github.com/pion/webrtc/v3 v3.0.31 + github.com/pion/webrtc/v3 v3.0.32 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.2.1 diff --git a/go.sum b/go.sum index e6bda4ba..a9a07f4d 100644 --- a/go.sum +++ b/go.sum @@ -314,9 +314,8 @@ github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXm github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= github.com/pion/dtls/v2 v2.0.9 h1:7Ow+V++YSZQMYzggI0P9vLJz/hUFcffsfGMfT/Qy+u8= github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQKYQho= -github.com/pion/ice/v2 v2.1.8/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= -github.com/pion/ice/v2 v2.1.9 h1:vl1X0PaR3qJjxuL6fsvsPlT9UOb7TCqEmHL9tMXJJMA= -github.com/pion/ice/v2 v2.1.9/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= +github.com/pion/ice/v2 v2.1.10 h1:Jt/BfUsaP+Dr6E5rbsy+w7w1JtHyFN0w2DkgfWq7Fko= +github.com/pion/ice/v2 v2.1.10/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= github.com/pion/interceptor v0.0.13 h1:fnV+b0p/KEzwwr/9z2nsSqA9IQRMsM4nF5HjrNSWwBo= github.com/pion/interceptor v0.0.13/go.mod h1:svsW2QoLHLoGLUr4pDoSopGBEWk8FZwlfxId/OKRKzo= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= @@ -347,8 +346,8 @@ github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= -github.com/pion/webrtc/v3 v3.0.31 h1:nKvgtsOpft7+QU7eObCFYwnRAtG/HHSmeXg1VljYs5M= -github.com/pion/webrtc/v3 v3.0.31/go.mod h1:aaGJVQNQ/e9lSXcOR3jEDHnMdG6Pg3HkUMd2tv2fvYM= +github.com/pion/webrtc/v3 v3.0.32 h1:5J+zNep9am8Swh6kEMp+LaGXNvn6qQWpGkLBnVW44L4= +github.com/pion/webrtc/v3 v3.0.32/go.mod h1:wX3V5dQQUGCifhT1mYftC2kCrDQX6ZJ3B7Yad0R9JK0= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= From e641ac57268d7744420666fe21cc8f8a7c652c12 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 27 Jul 2021 02:20:59 +1000 Subject: [PATCH 076/128] Dial logs (#398) * Add dial debug logging * Add env variable to enable debug logging --- Makefile | 2 +- internal/cmd/tunnel.go | 7 ++++ wsnet/dial.go | 90 ++++++++++++++++++++++++++++++++++++++---- wsnet/dial_test.go | 50 +++++++++++++++++------ 4 files changed, 127 insertions(+), 22 deletions(-) diff --git a/Makefile b/Makefile index edd1a303..2c8498bb 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Makefile for Coder CLI +# Makefile for Coder CLI .PHONY: clean build build/macos build/windows build/linux fmt lint gendocs test/go dev diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index 956e9fd2..da29e585 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -34,6 +34,10 @@ coder tunnel my-dev 3000 3000 RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() log := slog.Make(sloghuman.Sink(os.Stderr)) + if os.Getenv("CODER_TUNNEL_DEBUG") != "" { + log = log.Leveled(slog.LevelDebug) + log.Info(ctx, "debug logging enabled") + } remotePort, err := strconv.ParseUint(args[1], 10, 16) if err != nil { @@ -104,10 +108,13 @@ type tunnneler struct { func (c *tunnneler) start(ctx context.Context) error { c.log.Debug(ctx, "Connecting to workspace...") + + dialLog := c.log.Named("wsnet") wd, err := wsnet.DialWebsocket( ctx, wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token), &wsnet.DialOptions{ + Log: &dialLog, TURNProxyAuthToken: c.token, TURNProxyURL: c.brokerAddr, ICEServers: []webrtc.ICEServer{wsnet.TURNProxyICECandidate()}, diff --git a/wsnet/dial.go b/wsnet/dial.go index 3880b12c..b1f505a0 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -8,6 +8,7 @@ import ( "io" "net" "net/url" + "os" "sync" "time" @@ -16,11 +17,18 @@ import ( "golang.org/x/net/proxy" "nhooyr.io/websocket" + "cdr.dev/slog" + "cdr.dev/slog/sloggers/sloghuman" + "cdr.dev/coder-cli/coder-sdk" ) // DialOptions are configurable options for a wsnet connection. type DialOptions struct { + // Logger is an optional logger to use for logging mostly debug messages. If + // set to nil, nothing will be logged. + Log *slog.Logger + // ICEServers is an array of STUN or TURN servers to use for negotiation purposes. // See: https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration/iceServers ICEServers []webrtc.ICEServer @@ -36,6 +44,17 @@ type DialOptions struct { // DialWebsocket dials the broker with a WebSocket and negotiates a connection. func DialWebsocket(ctx context.Context, broker string, netOpts *DialOptions, wsOpts *websocket.DialOptions) (*Dialer, error) { + if netOpts == nil { + netOpts = &DialOptions{} + } + if netOpts.Log == nil { + // This logger will log nothing. + log := slog.Make() + netOpts.Log = &log + } + log := *netOpts.Log + + log.Debug(ctx, "connecting to broker", slog.F("broker", broker)) conn, resp, err := websocket.Dial(ctx, broker, wsOpts) if err != nil { if resp != nil { @@ -46,6 +65,8 @@ func DialWebsocket(ctx context.Context, broker string, netOpts *DialOptions, wsO } return nil, fmt.Errorf("dial websocket: %w", err) } + log.Debug(ctx, "connected to broker") + nconn := websocket.NetConn(ctx, conn, websocket.MessageBinary) defer func() { _ = nconn.Close() @@ -60,6 +81,11 @@ func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, er if options == nil { options = &DialOptions{} } + if options.Log == nil { + log := slog.Make(sloghuman.Sink(os.Stderr)).Leveled(slog.LevelInfo).Named("wsnet_dial") + options.Log = &log + } + log := *options.Log if options.ICEServers == nil { options.ICEServers = []webrtc.ICEServer{} } @@ -71,13 +97,20 @@ func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, er token: options.TURNProxyAuthToken, } } + + log.Debug(ctx, "creating peer connection", slog.F("options", options), slog.F("turn_proxy", turnProxy)) rtc, err := newPeerConnection(options.ICEServers, turnProxy) if err != nil { return nil, fmt.Errorf("create peer connection: %w", err) } + log.Debug(ctx, "created peer connection") + rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { + log.Debug(ctx, "connection state change", slog.F("state", pcs.String())) + }) flushCandidates := proxyICECandidates(rtc, conn) + log.Debug(ctx, "creating control channel", slog.F("proto", controlChannel)) ctrl, err := rtc.CreateDataChannel(controlChannel, &webrtc.DataChannelInit{ Protocol: stringPtr(controlChannel), Ordered: boolPtr(true), @@ -90,6 +123,7 @@ func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, er if err != nil { return nil, fmt.Errorf("create offer: %w", err) } + log.Debug(ctx, "created offer", slog.F("offer", offer)) err = rtc.SetLocalDescription(offer) if err != nil { return nil, fmt.Errorf("set local offer: %w", err) @@ -100,14 +134,17 @@ func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, er turnProxyURL = options.TURNProxyURL.String() } - offerMessage, err := json.Marshal(&BrokerMessage{ + bmsg := BrokerMessage{ Offer: &offer, Servers: options.ICEServers, TURNProxyURL: turnProxyURL, - }) + } + log.Debug(ctx, "sending offer message", slog.F("msg", bmsg)) + offerMessage, err := json.Marshal(&bmsg) if err != nil { return nil, fmt.Errorf("marshal offer message: %w", err) } + _, err = conn.Write(offerMessage) if err != nil { return nil, fmt.Errorf("write offer: %w", err) @@ -115,6 +152,7 @@ func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, er flushCandidates() dialer := &Dialer{ + log: log, conn: conn, ctrl: ctrl, rtc: rtc, @@ -128,6 +166,7 @@ func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, er // inside a workspace. The opposing end of the WebSocket messages // should be proxied with a Listener. type Dialer struct { + log slog.Logger conn net.Conn ctrl *webrtc.DataChannel ctrlrw datachannel.ReadWriteCloser @@ -152,20 +191,25 @@ func (d *Dialer) negotiate(ctx context.Context) (err error) { defer func() { _ = d.conn.Close() }() - err := waitForConnectionOpen(ctx, d.rtc) + + err := waitForConnectionOpen(context.Background(), d.rtc) if err != nil { + d.log.Debug(ctx, "negotiation error", slog.Error(err)) if errors.Is(err, context.DeadlineExceeded) { _ = d.conn.Close() } - errCh <- err + errCh <- fmt.Errorf("wait for connection to open: %w", err) return } + d.rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { if pcs == webrtc.PeerConnectionStateConnected { + d.log.Debug(ctx, "connected") return } // Close connections opened when RTC was alive. + d.log.Warn(ctx, "closing connections due to connection state change", slog.F("pcs", pcs.String())) d.connClosersMut.Lock() defer d.connClosersMut.Unlock() for _, connCloser := range d.connClosers { @@ -175,6 +219,7 @@ func (d *Dialer) negotiate(ctx context.Context) (err error) { }) }() + d.log.Debug(ctx, "beginning negotiation") for { var msg BrokerMessage err = decoder.Decode(&msg) @@ -184,6 +229,8 @@ func (d *Dialer) negotiate(ctx context.Context) (err error) { if err != nil { return fmt.Errorf("read: %w", err) } + d.log.Debug(ctx, "got message from handshake conn", slog.F("msg", msg)) + if msg.Candidate != "" { c := webrtc.ICECandidateInit{ Candidate: msg.Candidate, @@ -192,17 +239,22 @@ func (d *Dialer) negotiate(ctx context.Context) (err error) { pendingCandidates = append(pendingCandidates, c) continue } + + d.log.Debug(ctx, "adding remote ICE candidate", slog.F("c", c)) err = d.rtc.AddICECandidate(c) if err != nil { return fmt.Errorf("accept ice candidate: %s: %w", msg.Candidate, err) } continue } + if msg.Answer != nil { + d.log.Debug(ctx, "received answer", slog.F("a", *msg.Answer)) err = d.rtc.SetRemoteDescription(*msg.Answer) if err != nil { return fmt.Errorf("set answer: %w", err) } + for _, candidate := range pendingCandidates { err = d.rtc.AddICECandidate(candidate) if err != nil { @@ -212,11 +264,15 @@ func (d *Dialer) negotiate(ctx context.Context) (err error) { pendingCandidates = nil continue } + if msg.Error != "" { - return errors.New(msg.Error) + d.log.Debug(ctx, "got error from peer", slog.F("err", msg.Error)) + return fmt.Errorf("error from peer: %v", msg.Error) } + return fmt.Errorf("unhandled message: %+v", msg) } + return <-errCh } @@ -234,6 +290,7 @@ func (d *Dialer) activeConnections() int { // Close closes the RTC connection. // All data channels dialed will be closed. func (d *Dialer) Close() error { + d.log.Debug(context.Background(), "close called") return d.rtc.Close() } @@ -242,6 +299,7 @@ func (d *Dialer) Ping(ctx context.Context) error { if d.ctrl.ReadyState() == webrtc.DataChannelStateClosed || d.ctrl.ReadyState() == webrtc.DataChannelStateClosing { return webrtc.ErrConnectionClosed } + // Since we control the client and server we could open this // data channel with `Negotiated` true to reduce traffic being // sent when the RTC connection is opened. @@ -257,6 +315,7 @@ func (d *Dialer) Ping(ctx context.Context) error { } d.pingMut.Lock() defer d.pingMut.Unlock() + d.log.Debug(ctx, "sending ping") _, err = d.ctrlrw.Write([]byte{'a'}) if err != nil { return fmt.Errorf("write: %w", err) @@ -281,13 +340,18 @@ func (d *Dialer) Ping(ctx context.Context) error { // DialContext dials the network and address on the remote listener. func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + proto := fmt.Sprintf("%s:%s", network, address) + ctx = slog.With(ctx, slog.F("proto", proto)) + + d.log.Debug(ctx, "opening data channel") dc, err := d.rtc.CreateDataChannel("proxy", &webrtc.DataChannelInit{ Ordered: boolPtr(network != "udp"), - Protocol: stringPtr(fmt.Sprintf("%s:%s", network, address)), + Protocol: &proto, }) if err != nil { return nil, fmt.Errorf("create data channel: %w", err) } + d.connClosersMut.Lock() d.connClosers = append(d.connClosers, dc) d.connClosersMut.Unlock() @@ -296,10 +360,18 @@ func (d *Dialer) DialContext(ctx context.Context, network, address string) (net. if err != nil { return nil, fmt.Errorf("wait for open: %w", err) } + + ctx = slog.With(ctx, slog.F("dc_id", dc.ID())) + d.log.Debug(ctx, "data channel opened") + rw, err := dc.Detach() if err != nil { return nil, fmt.Errorf("detach: %w", err) } + d.log.Debug(ctx, "data channel detached") + + ctx, cancel := context.WithTimeout(ctx, time.Second*5) + defer cancel() errCh := make(chan error) go func() { @@ -309,6 +381,7 @@ func (d *Dialer) DialContext(ctx context.Context, network, address string) (net. errCh <- fmt.Errorf("read dial response: %w", err) return } + d.log.Debug(ctx, "dial response", slog.F("res", res)) if res.Err == "" { close(errCh) return @@ -323,8 +396,7 @@ func (d *Dialer) DialContext(ctx context.Context, network, address string) (net. } errCh <- err }() - ctx, cancel := context.WithTimeout(ctx, time.Second*5) - defer cancel() + select { case err := <-errCh: if err != nil { @@ -343,5 +415,7 @@ func (d *Dialer) DialContext(ctx context.Context, network, address string) (net. rw: rw, } c.init() + + d.log.Debug(ctx, "dial channel ready") return c, nil } diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 79e7ae39..2385e9a8 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -66,13 +66,16 @@ func TestDial(t *testing.T) { t.Run("Ping", func(t *testing.T) { t.Parallel() + log := slogtest.Make(t, nil) connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + l, err := Listen(context.Background(), log, listenAddr, "") require.NoError(t, err) defer l.Close() - dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ + Log: &log, + }, nil) require.NoError(t, err) err = dialer.Ping(context.Background()) @@ -81,14 +84,16 @@ func TestDial(t *testing.T) { t.Run("Ping Close", func(t *testing.T) { t.Parallel() + log := slogtest.Make(t, nil) connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + l, err := Listen(context.Background(), log, listenAddr, "") require.NoError(t, err) defer l.Close() turnAddr, closeTurn := createTURNServer(t, ice.SchemeTypeTURN) dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ + Log: &log, ICEServers: []webrtc.ICEServer{{ URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, Username: "example", @@ -101,18 +106,22 @@ func TestDial(t *testing.T) { _ = dialer.Ping(context.Background()) closeTurn() err = dialer.Ping(context.Background()) + assert.Error(t, err) assert.ErrorIs(t, err, io.EOF) }) t.Run("OPError", func(t *testing.T) { t.Parallel() + log := slogtest.Make(t, nil) connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + l, err := Listen(context.Background(), log, listenAddr, "") require.NoError(t, err) defer l.Close() - dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ + Log: &log, + }, nil) require.NoError(t, err) _, err = dialer.DialContext(context.Background(), "tcp", "localhost:100") @@ -125,6 +134,7 @@ func TestDial(t *testing.T) { t.Run("Proxy", func(t *testing.T) { t.Parallel() + log := slogtest.Make(t, nil) listener, err := net.Listen("tcp", "0.0.0.0:0") require.NoError(t, err) @@ -138,11 +148,13 @@ func TestDial(t *testing.T) { }() connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + l, err := Listen(context.Background(), log, listenAddr, "") require.NoError(t, err) defer l.Close() - dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ + Log: &log, + }, nil) require.NoError(t, err) conn, err := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) @@ -158,6 +170,7 @@ func TestDial(t *testing.T) { // Expect that we'd get an EOF on the server closing. t.Run("EOF on Close", func(t *testing.T) { t.Parallel() + log := slogtest.Make(t, nil) listener, err := net.Listen("tcp", "0.0.0.0:0") require.NoError(t, err) @@ -166,11 +179,13 @@ func TestDial(t *testing.T) { }() connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + l, err := Listen(context.Background(), log, listenAddr, "") require.NoError(t, err) defer l.Close() - dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ + Log: &log, + }, nil) require.NoError(t, err) conn, err := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) @@ -184,13 +199,16 @@ func TestDial(t *testing.T) { t.Run("Disconnect", func(t *testing.T) { t.Parallel() + log := slogtest.Make(t, nil) connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + l, err := Listen(context.Background(), log, listenAddr, "") require.NoError(t, err) defer l.Close() - dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ + Log: &log, + }, nil) require.NoError(t, err) err = dialer.Close() @@ -202,6 +220,7 @@ func TestDial(t *testing.T) { t.Run("Disconnect DialContext", func(t *testing.T) { t.Parallel() + log := slogtest.Make(t, nil) tcpListener, err := net.Listen("tcp", "0.0.0.0:0") require.NoError(t, err) @@ -210,12 +229,13 @@ func TestDial(t *testing.T) { }() connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + l, err := Listen(context.Background(), log, listenAddr, "") require.NoError(t, err) defer l.Close() turnAddr, closeTurn := createTURNServer(t, ice.SchemeTypeTURN) dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ + Log: &log, ICEServers: []webrtc.ICEServer{{ URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, Username: "example", @@ -237,6 +257,7 @@ func TestDial(t *testing.T) { t.Run("Active Connections", func(t *testing.T) { t.Parallel() + log := slogtest.Make(t, nil) listener, err := net.Listen("tcp", "0.0.0.0:0") if err != nil { @@ -252,7 +273,10 @@ func TestDial(t *testing.T) { t.Error(err) return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) + + dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ + Log: &log, + }, nil) if err != nil { t.Error(err) } From 6eb1887cd421331ed512c47c5e17f7991da6eb55 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Mon, 26 Jul 2021 12:59:59 -0500 Subject: [PATCH 077/128] Update last connection time on tunnel connections (#401) * Update last connection time on tunnnel connections * fmt --- coder-sdk/agent.go | 22 ++++++++++++++++++++++ coder-sdk/interface.go | 3 +++ internal/cmd/tunnel.go | 22 ++++++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 coder-sdk/agent.go diff --git a/coder-sdk/agent.go b/coder-sdk/agent.go new file mode 100644 index 00000000..29052f11 --- /dev/null +++ b/coder-sdk/agent.go @@ -0,0 +1,22 @@ +package coder + +import ( + "context" + "fmt" + "net/http" +) + +// UpdateLastConnectionAt updates the last connection at attribute of a workspace. +func (c *DefaultClient) UpdateLastConnectionAt(ctx context.Context, workspaceID string) error { + reqURL := fmt.Sprintf("/api/private/envagent/%s/update-last-connection-at", workspaceID) + resp, err := c.request(ctx, http.MethodPost, reqURL, nil) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return NewHTTPError(resp) + } + + return nil +} diff --git a/coder-sdk/interface.go b/coder-sdk/interface.go index dafb2114..b28bfe84 100644 --- a/coder-sdk/interface.go +++ b/coder-sdk/interface.go @@ -250,4 +250,7 @@ type Client interface { // DeleteSatelliteByID deletes a satellite entity from the Coder control plane. DeleteSatelliteByID(ctx context.Context, id string) error + + // UpdateLastConnectionAt updates the last connection at attribute of a workspace. + UpdateLastConnectionAt(ctx context.Context, workspaceID string) error } diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index da29e585..bc9a867f 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "strconv" + "time" "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" @@ -130,6 +131,27 @@ func (c *tunnneler) start(ctx context.Context) error { } c.log.Debug(ctx, "Connected to workspace!") + sdk, err := newClient(ctx, false) + if err != nil { + return xerrors.Errorf("getting coder client: %w", err) + } + + // regularly update the last connection at + go func() { + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + // silently ignore failures so we don't spam the console + _ = sdk.UpdateLastConnectionAt(ctx, c.workspaceID) + } + } + }() + // proxy via stdio if c.stdio { go func() { From 961f7ddfe6a391f73f0c7543014c055b6938fab8 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 27 Jul 2021 05:02:16 +1000 Subject: [PATCH 078/128] Warn if coder-cli is not in PATH on Windows (#357) * Warn if coder-cli not in PATH on Windows * Use safeexec to get windows path * Delete GOARCH=386 from windows build --- .github/workflows/release.yaml | 14 ++++----- Makefile | 2 +- ci/scripts/build.sh | 19 ++++++++---- go.mod | 1 + go.sum | 2 ++ internal/cmd/configssh.go | 55 +++++++++++++++++++++++++++++++--- 6 files changed, 75 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 19739a82..4a0606fb 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -22,8 +22,8 @@ jobs: - name: Upload windows uses: actions/upload-artifact@v2 with: - name: coder-cli-windows-386 - path: ./ci/bin/coder-cli-windows-386.zip + name: coder-cli-windows + path: ./ci/bin/coder-cli-windows.zip build_darwin: name: Build darwin binary runs-on: macos-latest @@ -74,7 +74,7 @@ jobs: draft: true prerelease: false - name: Upload Linux Release - id: upload-linux-release-asset + id: upload-linux-release-asset uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -84,7 +84,7 @@ jobs: asset_name: coder-cli-linux-amd64.tar.gz asset_content_type: application/tar+gzip - name: Upload MacOS Release - id: upload-macos-release-asset + id: upload-macos-release-asset uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -94,12 +94,12 @@ jobs: asset_name: coder-cli-darwin-amd64.zip asset_content_type: application/zip - name: Upload Windows Release - id: upload-windows-release-asset + id: upload-windows-release-asset uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: coder-cli-windows-386/coder-cli-windows-386.zip - asset_name: coder-cli-windows-386.zip + asset_path: coder-cli-windows/coder-cli-windows.zip + asset_name: coder-cli-windows.zip asset_content_type: application/zip diff --git a/Makefile b/Makefile index 2c8498bb..24c3d13e 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ build/macos: # requires darwin CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 ./ci/scripts/build.sh build/windows: - CGO_ENABLED=0 GOOS=windows GOARCH=386 ./ci/scripts/build.sh + CGO_ENABLED=0 GOOS=windows ./ci/scripts/build.sh build/linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 ./ci/scripts/build.sh diff --git a/ci/scripts/build.sh b/ci/scripts/build.sh index d19f1bf6..821c062e 100755 --- a/ci/scripts/build.sh +++ b/ci/scripts/build.sh @@ -8,11 +8,18 @@ set -euo pipefail cd "$(git rev-parse --show-toplevel)/ci/scripts" -tag=$(git describe --tags) - -echo "--- building coder-cli for $GOOS-$GOARCH" - -tmpdir=$(mktemp -d) +tag="$(git describe --tags)" + +flavor="$GOOS" +if [[ "$GOOS" == "windows" ]]; then + # GOARCH causes bugs with the safeexec package on Windows. + unset GOARCH +else + flavor+="-$GOARCH" +fi +echo "--- building coder-cli for $flavor" + +tmpdir="$(mktemp -d)" go build -ldflags "-X cdr.dev/coder-cli/internal/version.Version=${tag}" -o "$tmpdir/coder" ../../cmd/coder cp ../gon.json $tmpdir/gon.json @@ -20,7 +27,7 @@ cp ../gon.json $tmpdir/gon.json pushd "$tmpdir" case "$GOOS" in "windows") - artifact="coder-cli-$GOOS-$GOARCH.zip" + artifact="coder-cli-$GOOS.zip" mv coder coder.exe zip "$artifact" coder.exe ;; diff --git a/go.mod b/go.mod index 2539ea82..b4e02184 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( cdr.dev/slog v1.4.1 cdr.dev/wsep v0.0.0-20200728013649-82316a09813f github.com/briandowns/spinner v1.16.0 + github.com/cli/safeexec v1.0.0 github.com/fatih/color v1.12.0 github.com/google/go-cmp v0.5.6 github.com/gorilla/websocket v1.4.2 diff --git a/go.sum b/go.sum index a9a07f4d..30835c85 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= +github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index f4598837..4d233c36 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -12,13 +12,13 @@ import ( "sort" "strings" - "cdr.dev/coder-cli/pkg/clog" - + "github.com/cli/safeexec" "github.com/spf13/cobra" "golang.org/x/xerrors" "cdr.dev/coder-cli/coder-sdk" "cdr.dev/coder-cli/internal/coderutil" + "cdr.dev/coder-cli/pkg/clog" ) const sshStartToken = "# ------------START-CODER-ENTERPRISE-----------" @@ -114,7 +114,7 @@ func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []st return xerrors.New("SSH is disabled or not available for any workspaces in your Coder deployment.") } - binPath, err := os.Executable() + binPath, err := binPath() if err != nil { return xerrors.Errorf("Failed to get executable path: %w", err) } @@ -147,6 +147,53 @@ func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []st } } +// binPath returns the path to the coder binary suitable for use in ssh +// ProxyCommand. +func binPath() (string, error) { + exePath, err := os.Executable() + if err != nil { + return "", xerrors.Errorf("get executable path: %w", err) + } + + // On Windows, the coder-cli executable must be in $PATH for both Msys2/Git + // Bash and OpenSSH for Windows (used by Powershell and VS Code) to function + // correctly. Check if the current executable is in $PATH, and warn the user + // if it isn't. + if runtime.GOOS == "windows" { + binName := filepath.Base(exePath) + + // We use safeexec instead of os/exec because os/exec returns paths in + // the current working directory, which we will run into very often when + // looking for our own path. + pathPath, err := safeexec.LookPath(binName) + if err != nil { + clog.LogWarn( + "The current executable is not in $PATH.", + "This may lead to problems connecting to your workspace via SSH.", + fmt.Sprintf("Please move %q to a location in your $PATH (such as System32) and run `%s config-ssh` again.", binName, binName), + ) + // Return the exePath so SSH at least works outside of Msys2. + return exePath, nil + } + + // Warn the user if the current executable is not the same as the one in + // $PATH. + if filepath.Clean(pathPath) != filepath.Clean(exePath) { + clog.LogWarn( + "The current executable path does not match the executable path found in $PATH.", + "This may lead to problems connecting to your workspace via SSH.", + fmt.Sprintf("\t Current executable path: %q", exePath), + fmt.Sprintf("\tExecutable path in $PATH: %q", pathPath), + ) + } + + return binName, nil + } + + // On platforms other than Windows we can use the full path to the binary. + return exePath, nil +} + // removeOldConfig removes the old ssh configuration from the user's sshconfig. // Returns true if the config was modified. func removeOldConfig(config string) (string, bool) { @@ -212,7 +259,7 @@ func makeSSHConfig(binPath, host, userName, workspaceName, privateKeyFilepath st host := fmt.Sprintf( `Host coder.%s HostName coder.%s - ProxyCommand %s tunnel %s 12213 stdio + ProxyCommand "%s" tunnel %s 12213 stdio StrictHostKeyChecking no ConnectTimeout=0 IdentitiesOnly yes From d6730798eee4b27f1bd37b17f7ea55db06c4760d Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 27 Jul 2021 07:44:34 +1000 Subject: [PATCH 079/128] Add err wrapping to dialer (#402) --- wsnet/dial.go | 15 ++++++++++++++- wsnet/error.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 wsnet/error.go diff --git a/wsnet/dial.go b/wsnet/dial.go index b1f505a0..990ed118 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -104,6 +104,17 @@ func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, er return nil, fmt.Errorf("create peer connection: %w", err) } log.Debug(ctx, "created peer connection") + defer func() { + if err != nil { + // Wrap our error with some extra details. + err = errWrap{ + err: err, + iceServers: rtc.GetConfiguration().ICEServers, + rtc: rtc.ConnectionState(), + } + } + }() + rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { log.Debug(ctx, "connection state change", slog.F("state", pcs.String())) }) @@ -159,7 +170,9 @@ func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, er connClosers: []io.Closer{ctrl}, } - return dialer, dialer.negotiate(ctx) + // This is on a separate line so the defer above catches it. + err = dialer.negotiate(ctx) + return dialer, err } // Dialer enables arbitrary dialing to any network and address diff --git a/wsnet/error.go b/wsnet/error.go new file mode 100644 index 00000000..aa66d548 --- /dev/null +++ b/wsnet/error.go @@ -0,0 +1,39 @@ +package wsnet + +import ( + "fmt" + "strings" + + "github.com/pion/webrtc/v3" +) + +// errWrap wraps the error with some extra details about the state of the +// connection. +type errWrap struct { + err error + + iceServers []webrtc.ICEServer + rtc webrtc.PeerConnectionState +} + +var _ error = errWrap{} +var _ interface{ Unwrap() error } = errWrap{} + +// Error implements error. +func (e errWrap) Error() string { + return fmt.Sprintf("%v (ice: [%v], rtc: %v)", e.err.Error(), e.ice(), e.rtc.String()) +} + +func (e errWrap) ice() string { + msgs := []string{} + for _, s := range e.iceServers { + msgs = append(msgs, strings.Join(s.URLs, ", ")) + } + + return strings.Join(msgs, ", ") +} + +// Unwrap implements Unwrapper. +func (e errWrap) Unwrap() error { + return e.err +} From 175c42e0a8cae0f7a8322d74d8efe29b1c9bfd46 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 26 Jul 2021 18:07:05 -0600 Subject: [PATCH 080/128] fix: Only close connection scoped data channels on RTC close (#403) * fix: Only close connection scoped data channels on RTC close * Fix array pointer * Fix test * Remove old test --- wsnet/dial_test.go | 56 +++++++++++++++++++++++++++++----------------- wsnet/listen.go | 29 ++++++++++++++---------- 2 files changed, 53 insertions(+), 32 deletions(-) diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 2385e9a8..a9b09417 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -293,39 +293,55 @@ func TestDial(t *testing.T) { t.Run("Close Listeners on Disconnect", func(t *testing.T) { t.Parallel() + log := slogtest.Make(t, nil) - tcpListener, err := net.Listen("tcp", "0.0.0.0:0") + listener, err := net.Listen("tcp", "0.0.0.0:0") require.NoError(t, err) go func() { - _, _ = tcpListener.Accept() + for { + c, _ := listener.Accept() + + go func() { + b := make([]byte, 5) + _, err := c.Read(b) + if err != nil { + return + } + _, err = c.Write(b) + require.NoError(t, err) + }() + } }() - connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") + _, err = Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") require.NoError(t, err) - turnAddr, closeTurn := createTURNServer(t, ice.SchemeTypeTURN) - dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ - ICEServers: []webrtc.ICEServer{{ - URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, - Username: "example", - Credential: testPass, - CredentialType: webrtc.ICECredentialTypePassword, - }}, + d1, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ + Log: &log, }, nil) require.NoError(t, err) + _, err = d1.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) + require.NoError(t, err) - _, err = dialer.DialContext(context.Background(), "tcp", tcpListener.Addr().String()) + d2, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ + Log: &log, + }, nil) + require.NoError(t, err) + conn, err := d2.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) + require.NoError(t, err) + err = d1.Close() require.NoError(t, err) - closeTurn() + // TODO: This needs to be longer than the KeepAlive timeout for the RTC connection. + // Once the listener stores RTC connections instead of io.Closer we can directly + // reference the RTC connection to ensure it's properly closed. + time.Sleep(time.Second * 10) - list := l.(*listener) - assert.Eventually(t, func() bool { - list.connClosersMut.Lock() - defer list.connClosersMut.Unlock() - return len(list.connClosers) == 0 - }, time.Second*15, time.Millisecond*100) + b := []byte("hello") + _, err = conn.Write(b) + require.NoError(t, err) + _, err = conn.Read(b) + require.NoError(t, err) }) } diff --git a/wsnet/listen.go b/wsnet/listen.go index 803e140b..47ba808d 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -163,9 +163,11 @@ func (l *listener) negotiate(ctx context.Context, conn net.Conn) { ctx = slog.With(ctx, slog.F("conn_id", id)) var ( - err error - decoder = json.NewDecoder(conn) - rtc *webrtc.PeerConnection + err error + decoder = json.NewDecoder(conn) + rtc *webrtc.PeerConnection + connClosers = make([]io.Closer, 0) + connClosersMut sync.Mutex // If candidates are sent before an offer, we place them here. // We currently have no assurances to ensure this can't happen, // so it's better to buffer and process than fail. @@ -255,6 +257,9 @@ func (l *listener) negotiate(ctx context.Context, conn net.Conn) { closeError(err) return } + l.connClosersMut.Lock() + l.connClosers = append(l.connClosers, rtc) + l.connClosersMut.Unlock() rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { l.log.Info(ctx, "connection state change", slog.F("state", pcs.String())) switch pcs { @@ -267,16 +272,16 @@ func (l *listener) negotiate(ctx context.Context, conn net.Conn) { } // Close connections opened when RTC was alive. - l.connClosersMut.Lock() - defer l.connClosersMut.Unlock() - for _, connCloser := range l.connClosers { + connClosersMut.Lock() + defer connClosersMut.Unlock() + for _, connCloser := range connClosers { _ = connCloser.Close() } - l.connClosers = make([]io.Closer, 0) + connClosers = make([]io.Closer, 0) }) flushCandidates := proxyICECandidates(rtc, conn) - rtc.OnDataChannel(l.handle(ctx, msg)) + rtc.OnDataChannel(l.handle(ctx, msg, &connClosers, &connClosersMut)) l.log.Debug(ctx, "set remote description", slog.F("offer", *msg.Offer)) err = rtc.SetRemoteDescription(*msg.Offer) @@ -329,7 +334,7 @@ func (l *listener) negotiate(ctx context.Context, conn net.Conn) { } // nolint:gocognit -func (l *listener) handle(ctx context.Context, msg BrokerMessage) func(dc *webrtc.DataChannel) { +func (l *listener) handle(ctx context.Context, msg BrokerMessage, connClosers *[]io.Closer, connClosersMut *sync.Mutex) func(dc *webrtc.DataChannel) { return func(dc *webrtc.DataChannel) { if dc.Protocol() == controlChannel { // The control channel handles pings. @@ -430,9 +435,9 @@ func (l *listener) handle(ctx context.Context, msg BrokerMessage) func(dc *webrt dc: dc, rw: rw, } - l.connClosersMut.Lock() - l.connClosers = append(l.connClosers, co) - l.connClosersMut.Unlock() + connClosersMut.Lock() + *connClosers = append(*connClosers, co) + connClosersMut.Unlock() co.init() defer nc.Close() defer co.Close() From 802eac8e3a4dd448ee990a9f7479e275a433af4c Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 28 Jul 2021 13:08:08 -0600 Subject: [PATCH 081/128] feat: Add TURNLocalProxyURL to avoid roundtripping outside coderd (#405) --- internal/cmd/tunnel.go | 3 ++- wsnet/dial.go | 17 +++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index bc9a867f..b3a4ad06 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -117,7 +117,8 @@ func (c *tunnneler) start(ctx context.Context) error { &wsnet.DialOptions{ Log: &dialLog, TURNProxyAuthToken: c.token, - TURNProxyURL: c.brokerAddr, + TURNRemoteProxyURL: c.brokerAddr, + TURNLocalProxyURL: c.brokerAddr, ICEServers: []webrtc.ICEServer{wsnet.TURNProxyICECandidate()}, }, nil, diff --git a/wsnet/dial.go b/wsnet/dial.go index 990ed118..355d2c88 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -36,10 +36,11 @@ type DialOptions struct { // TURNProxyAuthToken is used to authenticate a TURN proxy request. TURNProxyAuthToken string - // TURNProxyURL is the URL to proxy all TURN data through. - // This URL is sent to the listener during handshake so both - // ends connect to the same TURN endpoint. - TURNProxyURL *url.URL + // TURNRemoteProxyURL is the URL to proxy listener TURN data through. + TURNRemoteProxyURL *url.URL + + // TURNLocalProxyURL is the URL to proxy client TURN data through. + TURNLocalProxyURL *url.URL } // DialWebsocket dials the broker with a WebSocket and negotiates a connection. @@ -91,9 +92,9 @@ func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, er } var turnProxy proxy.Dialer - if options.TURNProxyURL != nil { + if options.TURNLocalProxyURL != nil { turnProxy = &turnProxyDialer{ - baseURL: options.TURNProxyURL, + baseURL: options.TURNLocalProxyURL, token: options.TURNProxyAuthToken, } } @@ -141,8 +142,8 @@ func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, er } var turnProxyURL string - if options.TURNProxyURL != nil { - turnProxyURL = options.TURNProxyURL.String() + if options.TURNRemoteProxyURL != nil { + turnProxyURL = options.TURNRemoteProxyURL.String() } bmsg := BrokerMessage{ From 8e81679ceacde2fdf3325ac9a8e8bee4be645a83 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 30 Jul 2021 23:26:09 +0100 Subject: [PATCH 082/128] feat: allow hyphens and underscores in devurl names (#404) * allow hyphens and underscores in devurl names * update unit tests to use image with correct certs Co-authored-by: Dean Sheather --- internal/cmd/tags_test.go | 2 +- internal/cmd/urls.go | 15 ++++++++++----- internal/cmd/workspaces_test.go | 10 +++++----- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/internal/cmd/tags_test.go b/internal/cmd/tags_test.go index d074f43b..c04e7098 100644 --- a/internal/cmd/tags_test.go +++ b/internal/cmd/tags_test.go @@ -18,7 +18,7 @@ func Test_tags(t *testing.T) { res := execute(t, nil, "tags", "ls") res.error(t) - ensureImageImported(ctx, t, testCoderClient, "ubuntu") + ensureImageImported(ctx, t, testCoderClient, "ubuntu", "latest") res = execute(t, nil, "tags", "ls", "--image=ubuntu", "--org=default") res.success(t) diff --git a/internal/cmd/urls.go b/internal/cmd/urls.go index b6730d34..b20d1794 100644 --- a/internal/cmd/urls.go +++ b/internal/cmd/urls.go @@ -144,8 +144,8 @@ func createDevURLCmd() *cobra.Command { return xerrors.Errorf("invalid access level %q", access) } - if urlname != "" && !devURLNameValidRx.MatchString(urlname) { - return xerrors.New("update devurl: name must be < 64 chars in length, begin with a letter and only contain letters or digits.") + if urlname != "" && !devURLValidNameRx.MatchString(urlname) { + return xerrors.Errorf(devURLInvalidNameMsg, urlname) } client, err := newClient(ctx, true) if err != nil { @@ -199,9 +199,14 @@ func createDevURLCmd() *cobra.Command { } // devURLNameValidRx is the regex used to validate devurl names specified -// via the --name subcommand. Named devurls must begin with a letter, and -// consist solely of letters and digits, with a max length of 64 chars. -var devURLNameValidRx = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9]{0,63}$") +// via the --name subcommand. Named devurls must begin with a letter +// followed by zero or more letters, numbers, hyphens, or underscores, +// end with a letter or a number, and be maximum 64 characters in length. +// The maximum length of the name component is 43 characters. +var devURLValidNameRx = regexp.MustCompile("^[a-zA-Z]([a-zA-Z0-9_-]{0,41}[a-zA-Z0-9])?$") +var devURLInvalidNameMsg = "invalid devurl name %q: names must begin with a letter, " + + "followed by zero or more letters, digits, hyphens, or underscores, and end with a " + + "letter or digit, and be a maximum of 43 characters in length." // devURLID returns the ID of a devURL, given the workspace name and port // from a list of DevURL records. diff --git a/internal/cmd/workspaces_test.go b/internal/cmd/workspaces_test.go index a8c1d6cd..5dd3d01b 100644 --- a/internal/cmd/workspaces_test.go +++ b/internal/cmd/workspaces_test.go @@ -81,7 +81,7 @@ func Test_workspace_create(t *testing.T) { res.error(t) res.stderrContains(t, "fatal: image not found - did you forget to import this image?") - ensureImageImported(ctx, t, testCoderClient, "ubuntu") + ensureImageImported(ctx, t, testCoderClient, "codercom/enterprise-base", "ubuntu") name := randString(10) cpu := 2.3 @@ -89,7 +89,7 @@ func Test_workspace_create(t *testing.T) { // attempt to remove the workspace on cleanup t.Cleanup(func() { _ = execute(t, nil, "workspaces", "rm", name, "--force") }) - res = execute(t, nil, "workspaces", "create", name, "--image=ubuntu", fmt.Sprintf("--cpu=%f", cpu)) + res = execute(t, nil, "workspaces", "create", name, "--image=codercom/enterprise-base", "--tag=ubuntu", fmt.Sprintf("--cpu=%f", cpu)) res.success(t) res = execute(t, nil, "workspaces", "ls") @@ -108,7 +108,7 @@ func Test_workspace_create(t *testing.T) { // edit the CPU of the workspace cpu = 2.1 - res = execute(t, nil, "workspaces", "edit", name, fmt.Sprintf("--cpu=%f", cpu), "--follow", "--force") + res = execute(t, nil, "workspaces", "edit", name, "--image=codercom/enterprise-base", "--tag=ubuntu", fmt.Sprintf("--cpu=%f", cpu), "--follow", "--force") res.success(t) // assert that the CPU actually did change after edit @@ -153,7 +153,7 @@ var floatComparer = cmp.Comparer(func(x, y float64) bool { // this is a stopgap until we have support for a `coder images` subcommand // until then, we can use the coder.Client to ensure our integration tests // work on fresh deployments. -func ensureImageImported(ctx context.Context, t *testing.T, client coder.Client, img string) { +func ensureImageImported(ctx context.Context, t *testing.T, client coder.Client, img, tag string) { orgs, err := client.Organizations(ctx) assert.Success(t, "get orgs", err) @@ -198,7 +198,7 @@ search: RegistryID: &dockerhubID, OrgID: org.ID, Repository: img, - Tag: "latest", + Tag: tag, DefaultCPUCores: 2.5, DefaultDiskGB: 22, DefaultMemoryGB: 3, From 8da2581e9b98bd22780f05193a390714dcfd3348 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 3 Aug 2021 00:10:47 +1000 Subject: [PATCH 083/128] Use ICE servers from API (#406) --- Makefile | 2 +- coder-sdk/interface.go | 6 +++++- coder-sdk/webrtc.go | 23 +++++++++++++++++++++++ internal/cmd/tunnel.go | 10 +++++++++- 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 coder-sdk/webrtc.go diff --git a/Makefile b/Makefile index 24c3d13e..aa80df36 100644 --- a/Makefile +++ b/Makefile @@ -48,4 +48,4 @@ dev: build/linux -rm ./coder @echo "untarring..." @tar -xzf ./ci/bin/coder-cli-linux-amd64.tar.gz - @echo "new dev binary ready" \ No newline at end of file + @echo "new dev binary ready" diff --git a/coder-sdk/interface.go b/coder-sdk/interface.go index b28bfe84..6394b88b 100644 --- a/coder-sdk/interface.go +++ b/coder-sdk/interface.go @@ -5,6 +5,7 @@ import ( "net/url" "cdr.dev/wsep" + "github.com/pion/webrtc/v3" "nhooyr.io/websocket" ) @@ -242,7 +243,7 @@ type Client interface { // SetPolicyTemplate sets the workspace policy template SetPolicyTemplate(ctx context.Context, templateID string, templateScope TemplateScope, dryRun bool) (*SetPolicyTemplateResponse, error) - // satellites fetches all satellitess known to the Coder control plane. + // Satellites fetches all satellitess known to the Coder control plane. Satellites(ctx context.Context) ([]Satellite, error) // CreateSatellite creates a new satellite entity. @@ -253,4 +254,7 @@ type Client interface { // UpdateLastConnectionAt updates the last connection at attribute of a workspace. UpdateLastConnectionAt(ctx context.Context, workspaceID string) error + + // ICEServers fetches the list of ICE servers advertised by the deployment. + ICEServers(ctx context.Context) ([]webrtc.ICEServer, error) } diff --git a/coder-sdk/webrtc.go b/coder-sdk/webrtc.go new file mode 100644 index 00000000..4ea1713c --- /dev/null +++ b/coder-sdk/webrtc.go @@ -0,0 +1,23 @@ +package coder + +import ( + "context" + "net/http" + + "github.com/pion/webrtc/v3" +) + +type getICEServersRes struct { + Data []webrtc.ICEServer `json:"data"` +} + +// ICEServers fetches the list of ICE servers advertised by the deployment. +func (c *DefaultClient) ICEServers(ctx context.Context) ([]webrtc.ICEServer, error) { + var res getICEServersRes + err := c.requestBody(ctx, http.MethodGet, "/api/private/webrtc/ice", nil, &res) + if err != nil { + return nil, err + } + + return res.Data, nil +} diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index b3a4ad06..116ecfa0 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -75,11 +75,18 @@ coder tunnel my-dev 3000 3000 return xerrors.Errorf("No workspace found by name '%s'", args[0]) } + iceServers, err := sdk.ICEServers(ctx) + if err != nil { + return xerrors.Errorf("get ICE servers: %w", err) + } + log.Debug(ctx, "got ICE servers", slog.F("ice", iceServers)) + c := &tunnneler{ log: log, brokerAddr: &baseURL, token: sdk.Token(), workspaceID: workspaceID, + iceServers: iceServers, stdio: args[2] == "stdio", localPort: uint16(localPort), remotePort: uint16(remotePort), @@ -102,6 +109,7 @@ type tunnneler struct { brokerAddr *url.URL token string workspaceID string + iceServers []webrtc.ICEServer remotePort uint16 localPort uint16 stdio bool @@ -119,7 +127,7 @@ func (c *tunnneler) start(ctx context.Context) error { TURNProxyAuthToken: c.token, TURNRemoteProxyURL: c.brokerAddr, TURNLocalProxyURL: c.brokerAddr, - ICEServers: []webrtc.ICEServer{wsnet.TURNProxyICECandidate()}, + ICEServers: c.iceServers, }, nil, ) From edc273f61ee827994440208f2a5f30e433c68dca Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 2 Aug 2021 11:40:08 -0600 Subject: [PATCH 084/128] chore: Remove usage of netv2 flag for config-ssh (#407) * chore: Remove usage of netv2 flag for config-ssh * Remove access URL * Remove username * Remove username from caller * Remove username --- coder-sdk/workspace_providers.go | 1 - internal/cmd/configssh.go | 37 ++++++++------------------------ 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/coder-sdk/workspace_providers.go b/coder-sdk/workspace_providers.go index 5270389a..1ed0589c 100644 --- a/coder-sdk/workspace_providers.go +++ b/coder-sdk/workspace_providers.go @@ -19,7 +19,6 @@ type KubernetesProvider struct { EnvproxyAccessURL string `json:"envproxy_access_url" table:"Access URL" validate:"required"` DevurlHost string `json:"devurl_host" table:"Devurl Host"` OrgWhitelist []string `json:"org_whitelist" table:"-"` - EnableNetV2 bool `json:"enable_net_v2" table:"Enable NetV2"` KubeProviderConfig `json:"config" table:"_"` } diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index 4d233c36..3fc693d1 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io/ioutil" - "net/url" "os" "os/user" "path/filepath" @@ -119,7 +118,7 @@ func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []st return xerrors.Errorf("Failed to get executable path: %w", err) } - newConfig := makeNewConfigs(binPath, user.Username, workspacesWithProviders, privateKeyFilepath) + newConfig := makeNewConfigs(binPath, workspacesWithProviders, privateKeyFilepath) err = os.MkdirAll(filepath.Dir(*configpath), os.ModePerm) if err != nil { @@ -227,7 +226,7 @@ func writeSSHKey(ctx context.Context, client coder.Client, privateKeyPath string return ioutil.WriteFile(privateKeyPath, []byte(key.PrivateKey), 0600) } -func makeNewConfigs(binPath, userName string, workspaces []coderutil.WorkspaceWithWorkspaceProvider, privateKeyFilepath string) string { +func makeNewConfigs(binPath string, workspaces []coderutil.WorkspaceWithWorkspaceProvider, privateKeyFilepath string) string { newConfig := fmt.Sprintf("\n%s\n%s\n\n", sshStartToken, sshStartMessage) sort.Slice(workspaces, func(i, j int) bool { return workspaces[i].Workspace.Name < workspaces[j].Workspace.Name }) @@ -240,24 +239,17 @@ func makeNewConfigs(binPath, userName string, workspaces []coderutil.WorkspaceWi ) continue } - u, err := url.Parse(workspace.WorkspaceProvider.EnvproxyAccessURL) - if err != nil { - clog.LogWarn("invalid access url", clog.Causef("malformed url: %q", workspace.WorkspaceProvider.EnvproxyAccessURL)) - continue - } - useTunnel := workspace.WorkspaceProvider.SSHEnabled && workspace.WorkspaceProvider.EnableNetV2 - newConfig += makeSSHConfig(binPath, u.Host, userName, workspace.Workspace.Name, privateKeyFilepath, useTunnel) + newConfig += makeSSHConfig(binPath, workspace.Workspace.Name, privateKeyFilepath) } newConfig += fmt.Sprintf("\n%s\n", sshEndToken) return newConfig } -func makeSSHConfig(binPath, host, userName, workspaceName, privateKeyFilepath string, tunnel bool) string { - if tunnel { - host := fmt.Sprintf( - `Host coder.%s +func makeSSHConfig(binPath, workspaceName, privateKeyFilepath string) string { + entry := fmt.Sprintf( + `Host coder.%s HostName coder.%s ProxyCommand "%s" tunnel %s 12213 stdio StrictHostKeyChecking no @@ -266,25 +258,14 @@ func makeSSHConfig(binPath, host, userName, workspaceName, privateKeyFilepath st IdentityFile="%s" `, workspaceName, workspaceName, binPath, workspaceName, privateKeyFilepath) - if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { - host += ` ControlMaster auto + if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { + entry += ` ControlMaster auto ControlPath ~/.ssh/.connection-%r@%h:%p ControlPersist 600 ` - } - - return host } - return fmt.Sprintf( - `Host coder.%s - HostName %s - User %s-%s - StrictHostKeyChecking no - ConnectTimeout=0 - IdentitiesOnly yes - IdentityFile="%s" -`, workspaceName, host, userName, workspaceName, privateKeyFilepath) + return entry } func writeStr(filename, data string) error { From 3af83850be7f40f973c34b174c227e1905cec4ef Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 11:17:21 -0500 Subject: [PATCH 085/128] feat: Enable arbitrary SSH options passed via config-ssh (#410) * feat: Add option to disable SSH connection cache * Refactor to enable arbitrary options * Generate docs * Add comment for duplicated values --- docs/coder_config-ssh.md | 1 + internal/cmd/configssh.go | 55 +++++++++++++++++++++++---------------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/docs/coder_config-ssh.md b/docs/coder_config-ssh.md index 1816b29a..4114239f 100644 --- a/docs/coder_config-ssh.md +++ b/docs/coder_config-ssh.md @@ -15,6 +15,7 @@ coder config-ssh [flags] ``` --filepath string override the default path of your ssh config file (default "~/.ssh/config") -h, --help help for config-ssh + -o, --option strings additional options injected in the ssh config (ex. disable caching with "-o ControlPath=none") --remove remove the auto-generated Coder ssh config ``` diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index 3fc693d1..8818c9e2 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -33,23 +33,25 @@ const sshEndToken = "# ------------END-CODER-ENTERPRISE------------" func configSSHCmd() *cobra.Command { var ( - configpath string - remove = false + configpath string + remove = false + additionalOptions []string ) cmd := &cobra.Command{ Use: "config-ssh", Short: "Configure SSH to access Coder workspaces", Long: "Inject the proper OpenSSH configuration into your local SSH config file.", - RunE: configSSH(&configpath, &remove), + RunE: configSSH(&configpath, &remove, &additionalOptions), } cmd.Flags().StringVar(&configpath, "filepath", filepath.Join("~", ".ssh", "config"), "override the default path of your ssh config file") + cmd.Flags().StringSliceVarP(&additionalOptions, "option", "o", []string{}, "additional options injected in the ssh config (ex. disable caching with \"-o ControlPath=none\")") cmd.Flags().BoolVar(&remove, "remove", false, "remove the auto-generated Coder ssh config") return cmd } -func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []string) error { +func configSSH(configpath *string, remove *bool, additionalOptions *[]string) func(cmd *cobra.Command, _ []string) error { return func(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() usr, err := user.Current() @@ -118,7 +120,7 @@ func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []st return xerrors.Errorf("Failed to get executable path: %w", err) } - newConfig := makeNewConfigs(binPath, workspacesWithProviders, privateKeyFilepath) + newConfig := makeNewConfigs(binPath, workspacesWithProviders, privateKeyFilepath, *additionalOptions) err = os.MkdirAll(filepath.Dir(*configpath), os.ModePerm) if err != nil { @@ -226,7 +228,7 @@ func writeSSHKey(ctx context.Context, client coder.Client, privateKeyPath string return ioutil.WriteFile(privateKeyPath, []byte(key.PrivateKey), 0600) } -func makeNewConfigs(binPath string, workspaces []coderutil.WorkspaceWithWorkspaceProvider, privateKeyFilepath string) string { +func makeNewConfigs(binPath string, workspaces []coderutil.WorkspaceWithWorkspaceProvider, privateKeyFilepath string, additionalOptions []string) string { newConfig := fmt.Sprintf("\n%s\n%s\n\n", sshStartToken, sshStartMessage) sort.Slice(workspaces, func(i, j int) bool { return workspaces[i].Workspace.Name < workspaces[j].Workspace.Name }) @@ -240,32 +242,41 @@ func makeNewConfigs(binPath string, workspaces []coderutil.WorkspaceWithWorkspac continue } - newConfig += makeSSHConfig(binPath, workspace.Workspace.Name, privateKeyFilepath) + newConfig += makeSSHConfig(binPath, workspace.Workspace.Name, privateKeyFilepath, additionalOptions) } newConfig += fmt.Sprintf("\n%s\n", sshEndToken) return newConfig } -func makeSSHConfig(binPath, workspaceName, privateKeyFilepath string) string { - entry := fmt.Sprintf( - `Host coder.%s - HostName coder.%s - ProxyCommand "%s" tunnel %s 12213 stdio - StrictHostKeyChecking no - ConnectTimeout=0 - IdentitiesOnly yes - IdentityFile="%s" -`, workspaceName, workspaceName, binPath, workspaceName, privateKeyFilepath) +func makeSSHConfig(binPath, workspaceName, privateKeyFilepath string, additionalOptions []string) string { + // Custom user options come first to maximizessh customization. + options := []string{} + if len(additionalOptions) > 0 { + options = []string{ + "# Custom options. Duplicated values will always prefer the first!", + } + options = append(options, additionalOptions...) + options = append(options, "# End custom options.") + } + options = append(options, + fmt.Sprintf("HostName coder.%s", workspaceName), + fmt.Sprintf("ProxyCommand %q tunnel %s 12213 stdio", binPath, workspaceName), + "StrictHostKeyChecking no", + "ConnectTimeout=0", + "IdentitiesOnly yes", + fmt.Sprintf("IdentityFile=%q", privateKeyFilepath), + ) if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { - entry += ` ControlMaster auto - ControlPath ~/.ssh/.connection-%r@%h:%p - ControlPersist 600 -` + options = append(options, + "ControlMaster auto", + "ControlPath ~/.ssh/.connection-%r@%h:%p", + "ControlPersist 600", + ) } - return entry + return fmt.Sprintf("Host coder.%s\n\t%s\n\n", workspaceName, strings.Join(options, "\n\t")) } func writeStr(filename, data string) error { From 3536869fb86315ee664a1f775a4bac1a5319438e Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 11:56:47 -0500 Subject: [PATCH 086/128] feat: Display error to user when SSHing into offline workspace (#411) * feat: Display error to user when SSH'ing into offline workspace * Remove new \r * Update internal/cmd/tunnel.go Co-authored-by: Jonathan Yu Co-authored-by: Jonathan Yu --- go.mod | 3 +- go.sum | 6 +- internal/cmd/tunnel.go | 146 +++++++++++++++++++++++++++++++++-------- 3 files changed, 125 insertions(+), 30 deletions(-) diff --git a/go.mod b/go.mod index b4e02184..09be0cc6 100644 --- a/go.mod +++ b/go.mod @@ -25,9 +25,10 @@ require ( github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.2.1 github.com/stretchr/testify v1.7.0 + golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 golang.org/x/net v0.0.0-20210614182718-04defd469f4e golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 diff --git a/go.sum b/go.sum index 30835c85..de738e5e 100644 --- a/go.sum +++ b/go.sum @@ -430,8 +430,9 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -590,8 +591,9 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index 116ecfa0..203a9786 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -12,12 +12,15 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" + "github.com/fatih/color" "github.com/pion/webrtc/v3" "github.com/spf13/cobra" + "golang.org/x/crypto/ssh" "golang.org/x/xerrors" "cdr.dev/coder-cli/coder-sdk" "cdr.dev/coder-cli/internal/x/xcobra" + "cdr.dev/coder-cli/pkg/clog" "cdr.dev/coder-cli/wsnet" ) @@ -59,20 +62,34 @@ coder tunnel my-dev 3000 3000 } baseURL := sdk.BaseURL() - workspaces, err := getWorkspaces(ctx, sdk, coder.Me) + workspace, err := findWorkspace(ctx, sdk, args[0], coder.Me) if err != nil { return xerrors.Errorf("get workspaces: %w", err) } - var workspaceID string - for _, workspace := range workspaces { - if workspace.Name == args[0] { - workspaceID = workspace.ID - break + if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { + color.NoColor = false + notAvailableError := clog.Error("workspace not available", + fmt.Sprintf("current status: %q", workspace.LatestStat.ContainerStatus), + clog.BlankLine, + clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name), + ) + // If we're attempting to forward our remote SSH port, + // we want to communicate with the OpenSSH protocol so + // SSH clients can properly display output to our users. + if remotePort == 12213 { + rawKey, err := sdk.SSHKey(ctx) + if err != nil { + return xerrors.Errorf("get ssh key: %w", err) + } + err = discardSSHConnection(&stdioConn{}, rawKey.PrivateKey, notAvailableError.String()) + if err != nil { + return err + } + return nil } - } - if workspaceID == "" { - return xerrors.Errorf("No workspace found by name '%s'", args[0]) + + return notAvailableError } iceServers, err := sdk.ICEServers(ctx) @@ -82,14 +99,14 @@ coder tunnel my-dev 3000 3000 log.Debug(ctx, "got ICE servers", slog.F("ice", iceServers)) c := &tunnneler{ - log: log, - brokerAddr: &baseURL, - token: sdk.Token(), - workspaceID: workspaceID, - iceServers: iceServers, - stdio: args[2] == "stdio", - localPort: uint16(localPort), - remotePort: uint16(remotePort), + log: log, + brokerAddr: &baseURL, + token: sdk.Token(), + workspace: workspace, + iceServers: iceServers, + stdio: args[2] == "stdio", + localPort: uint16(localPort), + remotePort: uint16(remotePort), } err = c.start(ctx) @@ -105,14 +122,14 @@ coder tunnel my-dev 3000 3000 } type tunnneler struct { - log slog.Logger - brokerAddr *url.URL - token string - workspaceID string - iceServers []webrtc.ICEServer - remotePort uint16 - localPort uint16 - stdio bool + log slog.Logger + brokerAddr *url.URL + token string + workspace *coder.Workspace + iceServers []webrtc.ICEServer + remotePort uint16 + localPort uint16 + stdio bool } func (c *tunnneler) start(ctx context.Context) error { @@ -121,7 +138,7 @@ func (c *tunnneler) start(ctx context.Context) error { dialLog := c.log.Named("wsnet") wd, err := wsnet.DialWebsocket( ctx, - wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token), + wsnet.ConnectEndpoint(c.brokerAddr, c.workspace.ID, c.token), &wsnet.DialOptions{ Log: &dialLog, TURNProxyAuthToken: c.token, @@ -156,7 +173,7 @@ func (c *tunnneler) start(ctx context.Context) error { return case <-ticker.C: // silently ignore failures so we don't spam the console - _ = sdk.UpdateLastConnectionAt(ctx, c.workspaceID) + _ = sdk.UpdateLastConnectionAt(ctx, c.workspace.ID) } } }() @@ -203,3 +220,78 @@ func (c *tunnneler) start(ctx context.Context) error { }() } } + +// Used to treat stdio like a connection for proxying SSH. +type stdioConn struct{} + +func (s *stdioConn) Read(b []byte) (n int, err error) { + return os.Stdin.Read(b) +} + +func (s *stdioConn) Write(b []byte) (n int, err error) { + return os.Stdout.Write(b) +} + +func (s *stdioConn) Close() error { + return nil +} + +func (s *stdioConn) LocalAddr() net.Addr { + return nil +} + +func (s *stdioConn) RemoteAddr() net.Addr { + return nil +} + +func (s *stdioConn) SetDeadline(t time.Time) error { + return nil +} + +func (s *stdioConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (s *stdioConn) SetWriteDeadline(t time.Time) error { + return nil +} + +// discardSSHConnection accepts a connection then outputs the message provided +// to any channel opened, immediately closing the connection afterwards. +// +// Used to provide status to connecting clients while still aligning with the +// native SSH protocol. +func discardSSHConnection(nc net.Conn, privateKey string, msg string) error { + config := &ssh.ServerConfig{ + NoClientAuth: true, + } + key, err := ssh.ParseRawPrivateKey([]byte(privateKey)) + if err != nil { + return fmt.Errorf("parse private key: %w", err) + } + signer, err := ssh.NewSignerFromKey(key) + if err != nil { + return fmt.Errorf("signer from private key: %w", err) + } + config.AddHostKey(signer) + conn, chans, reqs, err := ssh.NewServerConn(nc, config) + if err != nil { + return fmt.Errorf("create server conn: %w", err) + } + go ssh.DiscardRequests(reqs) + ch, req, err := (<-chans).Accept() + if err != nil { + return fmt.Errorf("accept channel: %w", err) + } + go ssh.DiscardRequests(req) + + _, err = ch.Write([]byte(msg)) + if err != nil { + return fmt.Errorf("write channel: %w", err) + } + err = ch.Close() + if err != nil { + return fmt.Errorf("close channel: %w", err) + } + return conn.Close() +} From 9244ee8addaefa5c13431c3d1b60421edfad7ac3 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 13:32:30 -0500 Subject: [PATCH 087/128] feat: Add Ping command to monitor workspace latency (#409) * feat: Add Ping command to monitor workspace latency * Handle shut off with nice error * Organize funcs * Add docs * Organize imports * Move to subdommand of workspaces * Refactor to be smarter * Enable scheme filtering * Add count flag * Fix import order * Disable linting for nested if * Generate docs * Extract funcs * Update docs * Remove receiver --- docs/coder_workspaces.md | 1 + docs/coder_workspaces_ping.md | 36 ++++++ internal/cmd/cmd.go | 26 ++-- internal/cmd/workspaces.go | 220 +++++++++++++++++++++++++++++++++- wsnet/dial.go | 5 + 5 files changed, 269 insertions(+), 19 deletions(-) create mode 100644 docs/coder_workspaces_ping.md diff --git a/docs/coder_workspaces.md b/docs/coder_workspaces.md index a7ec4615..936db713 100644 --- a/docs/coder_workspaces.md +++ b/docs/coder_workspaces.md @@ -26,6 +26,7 @@ Perform operations on the Coder workspaces owned by the active user. * [coder workspaces edit](coder_workspaces_edit.md) - edit an existing workspace and initiate a rebuild. * [coder workspaces edit-from-config](coder_workspaces_edit-from-config.md) - change the template a workspace is tracking * [coder workspaces ls](coder_workspaces_ls.md) - list all workspaces owned by the active user +* [coder workspaces ping](coder_workspaces_ping.md) - ping Coder workspaces by name * [coder workspaces policy-template](coder_workspaces_policy-template.md) - Set workspace policy template * [coder workspaces rebuild](coder_workspaces_rebuild.md) - rebuild a Coder workspace * [coder workspaces rm](coder_workspaces_rm.md) - remove Coder workspaces by name diff --git a/docs/coder_workspaces_ping.md b/docs/coder_workspaces_ping.md new file mode 100644 index 00000000..cf55fff1 --- /dev/null +++ b/docs/coder_workspaces_ping.md @@ -0,0 +1,36 @@ +## coder workspaces ping + +ping Coder workspaces by name + +### Synopsis + +ping Coder workspaces by name + +``` +coder workspaces ping [flags] +``` + +### Examples + +``` +coder workspaces ping front-end-workspace +``` + +### Options + +``` + -c, --count int stop after replies + -h, --help help for ping + -s, --scheme strings customize schemes to filter ice servers (default [stun,stuns,turn,turns]) +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces + diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 4ad33209..26df6bc4 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -22,25 +22,25 @@ func Make() *cobra.Command { } app.AddCommand( + agentCmd(), + completionCmd(), + configSSHCmd(), + envCmd(), // DEPRECATED. + genDocsCmd(app), + imgsCmd(), loginCmd(), logoutCmd(), + providersCmd(), + resourceCmd(), + satellitesCmd(), sshCmd(), - usersCmd(), - tagsCmd(), - configSSHCmd(), - envCmd(), // DEPRECATED. - workspacesCmd(), syncCmd(), - urlCmd(), + tagsCmd(), tokensCmd(), - resourceCmd(), - completionCmd(), - imgsCmd(), - providersCmd(), - genDocsCmd(app), - agentCmd(), tunnelCmd(), - satellitesCmd(), + urlCmd(), + usersCmd(), + workspacesCmd(), ) app.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show verbose output") return app diff --git a/internal/cmd/workspaces.go b/internal/cmd/workspaces.go index d1135cf4..ff70b64d 100644 --- a/internal/cmd/workspaces.go +++ b/internal/cmd/workspaces.go @@ -4,17 +4,27 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "io/ioutil" + "os" + "strings" + "time" + + "nhooyr.io/websocket" "cdr.dev/coder-cli/coder-sdk" "cdr.dev/coder-cli/internal/coderutil" "cdr.dev/coder-cli/internal/x/xcobra" "cdr.dev/coder-cli/pkg/clog" "cdr.dev/coder-cli/pkg/tablewriter" + "cdr.dev/coder-cli/wsnet" + "github.com/fatih/color" "github.com/manifoldco/promptui" + "github.com/pion/ice/v2" + "github.com/pion/webrtc/v3" "github.com/spf13/cobra" "golang.org/x/xerrors" ) @@ -38,16 +48,17 @@ func workspacesCmd() *cobra.Command { } cmd.AddCommand( + createWorkspaceCmd(), + editWorkspaceCmd(), lsWorkspacesCommand(), - stopWorkspacesCmd(), + pingWorkspaceCommand(), + rebuildWorkspaceCommand(), rmWorkspacesCmd(), + setPolicyTemplate(), + stopWorkspacesCmd(), watchBuildLogCommand(), - rebuildWorkspaceCommand(), - createWorkspaceCmd(), - workspaceFromConfigCmd(true), workspaceFromConfigCmd(false), - editWorkspaceCmd(), - setPolicyTemplate(), + workspaceFromConfigCmd(true), ) return cmd } @@ -120,6 +131,203 @@ func lsWorkspacesCommand() *cobra.Command { return cmd } +func pingWorkspaceCommand() *cobra.Command { + var ( + schemes []string + count int + ) + + cmd := &cobra.Command{ + Use: "ping ", + Short: "ping Coder workspaces by name", + Long: "ping Coder workspaces by name", + Example: `coder workspaces ping front-end-workspace`, + Args: xcobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + client, err := newClient(ctx, true) + if err != nil { + return err + } + workspace, err := findWorkspace(ctx, client, args[0], coder.Me) + if err != nil { + return err + } + + iceSchemes := map[ice.SchemeType]interface{}{} + for _, rawScheme := range schemes { + scheme := ice.NewSchemeType(rawScheme) + if scheme == ice.Unknown { + return fmt.Errorf("scheme type %q not recognized", rawScheme) + } + iceSchemes[scheme] = nil + } + + pinger := &wsPinger{ + client: client, + workspace: workspace, + iceSchemes: iceSchemes, + } + + seq := 0 + ticker := time.NewTicker(time.Second) + for { + select { + case <-ticker.C: + err := pinger.ping(ctx) + if err != nil { + return err + } + seq++ + if count > 0 && seq >= count { + os.Exit(0) + } + case <-ctx.Done(): + return nil + } + } + }, + } + + cmd.Flags().StringSliceVarP(&schemes, "scheme", "s", []string{"stun", "stuns", "turn", "turns"}, "customize schemes to filter ice servers") + cmd.Flags().IntVarP(&count, "count", "c", 0, "stop after replies") + return cmd +} + +type wsPinger struct { + client coder.Client + workspace *coder.Workspace + dialer *wsnet.Dialer + iceSchemes map[ice.SchemeType]interface{} + tunneled bool +} + +func (*wsPinger) logFail(msg string) { + fmt.Printf("%s: %s\n", color.New(color.Bold, color.FgRed).Sprint("——"), msg) +} + +func (*wsPinger) logSuccess(timeStr, msg string) { + fmt.Printf("%s: %s\n", color.New(color.Bold, color.FgGreen).Sprint(timeStr), msg) +} + +// Only return fatal errors +func (w *wsPinger) ping(ctx context.Context) error { + ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) + defer cancelFunc() + url := w.client.BaseURL() + + // If the dialer is nil we create a new! + // nolint:nestif + if w.dialer == nil { + servers, err := w.client.ICEServers(ctx) + if err != nil { + w.logFail(fmt.Sprintf("list ice servers: %s", err.Error())) + return nil + } + filteredServers := make([]webrtc.ICEServer, 0, len(servers)) + for _, server := range servers { + good := true + for _, rawURL := range server.URLs { + url, err := ice.ParseURL(rawURL) + if err != nil { + return fmt.Errorf("parse url %q: %w", rawURL, err) + } + if _, ok := w.iceSchemes[url.Scheme]; !ok { + good = false + } + } + if good { + filteredServers = append(filteredServers, server) + } + } + if len(filteredServers) == 0 { + schemes := make([]string, 0) + for scheme := range w.iceSchemes { + schemes = append(schemes, scheme.String()) + } + return fmt.Errorf("no ice servers match the schemes provided: %s", strings.Join(schemes, ",")) + } + workspace, err := w.client.WorkspaceByID(ctx, w.workspace.ID) + if err != nil { + return err + } + if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { + w.logFail(fmt.Sprintf("workspace is unreachable (status=%s)", workspace.LatestStat.ContainerStatus)) + return nil + } + connectStart := time.Now() + w.dialer, err = wsnet.DialWebsocket(ctx, wsnet.ConnectEndpoint(&url, w.workspace.ID, w.client.Token()), &wsnet.DialOptions{ + ICEServers: filteredServers, + TURNProxyAuthToken: w.client.Token(), + TURNRemoteProxyURL: &url, + TURNLocalProxyURL: &url, + }, &websocket.DialOptions{}) + if err != nil { + w.logFail(fmt.Sprintf("dial workspace: %s", err.Error())) + return nil + } + connectMS := float64(time.Since(connectStart).Microseconds()) / 1000 + + candidates, err := w.dialer.Candidates() + if err != nil { + return err + } + isRelaying := candidates.Local.Typ == webrtc.ICECandidateTypeRelay + w.tunneled = false + candidateURLs := []string{} + + for _, server := range filteredServers { + if server.Username == wsnet.TURNProxyICECandidate().Username { + candidateURLs = append(candidateURLs, fmt.Sprintf("turn:%s", url.Host)) + if !isRelaying { + continue + } + w.tunneled = true + continue + } + + candidateURLs = append(candidateURLs, server.URLs...) + } + + connectionText := "direct via STUN" + if isRelaying { + connectionText = "proxied via TURN" + } + if w.tunneled { + connectionText = fmt.Sprintf("proxied via %s", url.Host) + } + w.logSuccess("——", fmt.Sprintf( + "connected in %.2fms (%s) candidates=%s", + connectMS, + connectionText, + strings.Join(candidateURLs, ","), + )) + } + + pingStart := time.Now() + err := w.dialer.Ping(ctx) + if err != nil { + if errors.Is(err, io.EOF) { + w.dialer = nil + w.logFail("connection timed out") + return nil + } + if errors.Is(err, webrtc.ErrConnectionClosed) { + w.dialer = nil + w.logFail("webrtc connection is closed") + return nil + } + return fmt.Errorf("ping workspace: %w", err) + } + pingMS := float64(time.Since(pingStart).Microseconds()) / 1000 + connectionText := "you ↔ workspace" + if w.tunneled { + connectionText = fmt.Sprintf("you ↔ %s ↔ workspace", url.Host) + } + w.logSuccess(fmt.Sprintf("%.2fms", pingMS), connectionText) + return nil +} + func stopWorkspacesCmd() *cobra.Command { var user string cmd := &cobra.Command{ diff --git a/wsnet/dial.go b/wsnet/dial.go index 355d2c88..35e13870 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -301,6 +301,11 @@ func (d *Dialer) activeConnections() int { return int(stats.DataChannelsRequested-stats.DataChannelsClosed) - 1 } +// Candidates returns the candidate pair that was chosen for the connection. +func (d *Dialer) Candidates() (*webrtc.ICECandidatePair, error) { + return d.rtc.SCTP().Transport().ICETransport().GetSelectedCandidatePair() +} + // Close closes the RTC connection. // All data channels dialed will be closed. func (d *Dialer) Close() error { From 3ebd10262f4e95bb7cdc37219f9f6eb85c4e3a35 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 10 Aug 2021 20:12:11 -0500 Subject: [PATCH 088/128] fix: Continue attempting connections no matter what (#412) * fix: Continue accepting connections after yamux session EOFs * Add verbosity to timeout * Revert test * Revert test change * Never exit when connection fails * Remove useless log --- internal/cmd/agent.go | 1 + wsnet/listen.go | 10 +++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go index 29f65b21..14bd11ab 100644 --- a/internal/cmd/agent.go +++ b/internal/cmd/agent.go @@ -84,6 +84,7 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx return xerrors.Errorf("listen: %w", err) } defer func() { + log.Info(ctx, "closing wsnet listener") err := listener.Close() if err != nil { log.Error(ctx, "close listener", slog.Error(err)) diff --git a/wsnet/listen.go b/wsnet/listen.go index 47ba808d..78002899 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -71,7 +71,7 @@ func Listen(ctx context.Context, log slog.Logger, broker string, turnProxyAuthTo default: } - if errors.Is(err, io.EOF) || errors.Is(err, yamux.ErrKeepAliveTimeout) { + if err != nil { l.log.Warn(ctx, "disconnected from broker", slog.Error(err)) // If we hit an EOF, then the connection to the broker @@ -88,14 +88,11 @@ func Listen(ctx context.Context, log slog.Logger, broker string, turnProxyAuthTo if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { break } + l.log.Warn(ctx, "connecting to broker failed", slog.Error(err)) } ticker.Stop() } - if err != nil { - l.acceptError = err - _ = l.Close() - break - } + l.log.Info(ctx, "connected to broker") } }() return l, nil @@ -106,7 +103,6 @@ type listener struct { turnProxyAuthToken string log slog.Logger - acceptError error ws *websocket.Conn connClosers []io.Closer connClosersMut sync.Mutex From 055521dbf65aab3d64f7700c8d2bda3a06b28750 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Aug 2021 02:15:07 -0500 Subject: [PATCH 089/128] chore: bump github.com/pion/ice/v2 from 2.1.10 to 2.1.12 (#415) Bumps [github.com/pion/ice/v2](https://github.com/pion/ice) from 2.1.10 to 2.1.12. - [Release notes](https://github.com/pion/ice/releases) - [Commits](https://github.com/pion/ice/compare/v2.1.10...v2.1.12) --- updated-dependencies: - dependency-name: github.com/pion/ice/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 09be0cc6..db32d750 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/pion/datachannel v1.4.21 github.com/pion/dtls/v2 v2.0.9 - github.com/pion/ice/v2 v2.1.10 + github.com/pion/ice/v2 v2.1.12 github.com/pion/logging v0.2.2 github.com/pion/turn/v2 v2.0.5 github.com/pion/webrtc/v3 v3.0.32 @@ -26,7 +26,7 @@ require ( github.com/spf13/cobra v1.2.1 github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 - golang.org/x/net v0.0.0-20210614182718-04defd469f4e + golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 diff --git a/go.sum b/go.sum index de738e5e..755babf6 100644 --- a/go.sum +++ b/go.sum @@ -205,8 +205,9 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -316,8 +317,9 @@ github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXm github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= github.com/pion/dtls/v2 v2.0.9 h1:7Ow+V++YSZQMYzggI0P9vLJz/hUFcffsfGMfT/Qy+u8= github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQKYQho= -github.com/pion/ice/v2 v2.1.10 h1:Jt/BfUsaP+Dr6E5rbsy+w7w1JtHyFN0w2DkgfWq7Fko= github.com/pion/ice/v2 v2.1.10/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= +github.com/pion/ice/v2 v2.1.12 h1:ZDBuZz+fEI7iDifZCYFVzI4p0Foy0YhdSSZ87ZtRcRE= +github.com/pion/ice/v2 v2.1.12/go.mod h1:ovgYHUmwYLlRvcCLI67PnQ5YGe+upXZbGgllBDG/ktU= github.com/pion/interceptor v0.0.13 h1:fnV+b0p/KEzwwr/9z2nsSqA9IQRMsM4nF5HjrNSWwBo= github.com/pion/interceptor v0.0.13/go.mod h1:svsW2QoLHLoGLUr4pDoSopGBEWk8FZwlfxId/OKRKzo= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= @@ -510,8 +512,9 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= From f98e94cd3418961d7af051233c8928ec479c88de Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 12 Aug 2021 09:16:01 -0500 Subject: [PATCH 090/128] chore: Use webrtc for coder sh (#408) - Use webrtc tunnel for `coder sh` command --- internal/cmd/configssh.go | 9 ++++++++- internal/cmd/ssh.go | 22 +++++++++++++--------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index 8818c9e2..9740a102 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -261,7 +261,7 @@ func makeSSHConfig(binPath, workspaceName, privateKeyFilepath string, additional } options = append(options, fmt.Sprintf("HostName coder.%s", workspaceName), - fmt.Sprintf("ProxyCommand %q tunnel %s 12213 stdio", binPath, workspaceName), + fmt.Sprintf("ProxyCommand %s", proxyCommand(binPath, workspaceName, true)), "StrictHostKeyChecking no", "ConnectTimeout=0", "IdentitiesOnly yes", @@ -279,6 +279,13 @@ func makeSSHConfig(binPath, workspaceName, privateKeyFilepath string, additional return fmt.Sprintf("Host coder.%s\n\t%s\n\n", workspaceName, strings.Join(options, "\n\t")) } +func proxyCommand(binPath, workspaceName string, quoted bool) string { + if quoted { + binPath = fmt.Sprintf("%q", binPath) + } + return fmt.Sprintf(`%s tunnel %s 12213 stdio`, binPath, workspaceName) +} + func writeStr(filename, data string) error { return ioutil.WriteFile(filename, []byte(data), 0777) } diff --git a/internal/cmd/ssh.go b/internal/cmd/ssh.go index cec588a6..983ec3f4 100644 --- a/internal/cmd/ssh.go +++ b/internal/cmd/ssh.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "net/url" "os" "os/exec" "os/user" @@ -41,10 +40,7 @@ func shell(cmd *cobra.Command, args []string) error { if err != nil { return err } - me, err := client.Me(ctx) - if err != nil { - return err - } + workspace, err := findWorkspace(ctx, client, args[0], coder.Me) if err != nil { return err @@ -60,9 +56,9 @@ func shell(cmd *cobra.Command, args []string) error { if err != nil { return err } - u, err := url.Parse(wp.EnvproxyAccessURL) - if err != nil { - return err + + if !wp.SSHEnabled { + return clog.Error("SSH is disabled on this Workspace") } usr, err := user.Current() @@ -75,13 +71,21 @@ func shell(cmd *cobra.Command, args []string) error { if err != nil { return err } + + binPath, err := binPath() + if err != nil { + return xerrors.Errorf("Failed to get executable path: %w", err) + } + ssh := exec.CommandContext(ctx, "ssh", "-i"+privateKeyFilepath, - fmt.Sprintf("%s-%s@%s", me.Username, workspace.Name, u.Hostname()), + "-o"+fmt.Sprintf("ProxyCommand=%s", proxyCommand(binPath, workspace.Name, false)), + workspace.Name, ) if len(args) > 1 { ssh.Args = append(ssh.Args, args[1:]...) } + ssh.Stderr = os.Stderr ssh.Stdout = os.Stdout ssh.Stdin = os.Stdin From 753937ad4995b849b20cd043fafecbb926578cc9 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 12 Aug 2021 09:21:35 -0500 Subject: [PATCH 091/128] chore: Add line returns to clog output (#414) --- pkg/clog/clog.go | 7 +++---- pkg/clog/clog_test.go | 8 ++++---- pkg/clog/errgroup_test.go | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/pkg/clog/clog.go b/pkg/clog/clog.go index 0a523e1f..ebdce686 100644 --- a/pkg/clog/clog.go +++ b/pkg/clog/clog.go @@ -35,12 +35,11 @@ type CLIError struct { // String formats the CLI message for consumption by a human. func (m CLIMessage) String() string { var str strings.Builder - str.WriteString(fmt.Sprintf("%s: %s\n", + fmt.Fprintf(&str, "%s: %s\r\n", color.New(m.Color).Sprint(m.Level), - color.New(color.Bold).Sprint(m.Header)), - ) + color.New(color.Bold).Sprint(m.Header)) for _, line := range m.Lines { - str.WriteString(fmt.Sprintf(" %s %s\n", color.New(m.Color).Sprint("|"), line)) + fmt.Fprintf(&str, " %s %s\r\n", color.New(m.Color).Sprint("|"), line) } return str.String() } diff --git a/pkg/clog/clog_test.go b/pkg/clog/clog_test.go index 51eab07e..6967eb3c 100644 --- a/pkg/clog/clog_test.go +++ b/pkg/clog/clog_test.go @@ -25,7 +25,7 @@ func TestError(t *testing.T) { output, err := ioutil.ReadAll(&buf) assert.Success(t, "read all stderr output", err) - assert.Equal(t, "output is as expected", "error: fake error\n\n", string(output)) + assert.Equal(t, "output is as expected", "error: fake error\r\n\n", string(output)) }) t.Run("plain-error", func(t *testing.T) { @@ -41,7 +41,7 @@ func TestError(t *testing.T) { output, err := ioutil.ReadAll(&buf) assert.Success(t, "read all stderr output", err) - assert.Equal(t, "output is as expected", "fatal: wrap 1: base error\n\n", string(output)) + assert.Equal(t, "output is as expected", "fatal: wrap 1: base error\r\n\n", string(output)) }) t.Run("message", func(t *testing.T) { @@ -58,7 +58,7 @@ func TestError(t *testing.T) { output, err := ioutil.ReadAll(&buf) assert.Success(t, "read all stderr output", err) - assert.Equal(t, "output is as expected", f.level+": testing\n | hint: maybe do \"this\"\n | \n | cause: what happened was \"this\"\n", string(output)) + assert.Equal(t, "output is as expected", f.level+": testing\r\n | hint: maybe do \"this\"\r\n | \r\n | cause: what happened was \"this\"\r\n", string(output)) } }) @@ -78,7 +78,7 @@ func TestError(t *testing.T) { assert.Equal(t, "output is as expected", - "error: fake header\n | next line\n | \n | tip: content of fake tip\n\n", + "error: fake header\r\n | next line\r\n | \r\n | tip: content of fake tip\r\n\n", string(output), ) }) diff --git a/pkg/clog/errgroup_test.go b/pkg/clog/errgroup_test.go index b632921d..0d209d81 100644 --- a/pkg/clog/errgroup_test.go +++ b/pkg/clog/errgroup_test.go @@ -37,7 +37,7 @@ func TestErrGroup(t *testing.T) { err := egroup.Wait() assert.ErrorContains(t, "error group wait", err, "2 failures emitted") - assert.True(t, "log buf contains", strings.Contains(buf.String(), "fatal: whoops\n\n")) - assert.True(t, "log buf contains", strings.Contains(buf.String(), "error: rich error\n | second line\n\n")) + assert.True(t, "log buf contains", strings.Contains(buf.String(), "fatal: whoops\r\n\n")) + assert.True(t, "log buf contains", strings.Contains(buf.String(), "error: rich error\r\n | second line\r\n\n")) }) } From cf9e7e411a5a6aaab34f0135091eaaf7c06aec1a Mon Sep 17 00:00:00 2001 From: goodspark Date: Thu, 12 Aug 2021 14:07:55 -0700 Subject: [PATCH 092/128] feat: Option to list all workspaces (#416) * Option to list all workspaces From an admin POV, this will be helpful for various reasons. * Update internal/cmd/workspaces.go Co-authored-by: Dean Sheather * Fixes * Update internal/cmd/workspaces.go Co-authored-by: Dean Sheather * Fix lint Co-authored-by: Dean Sheather --- internal/cmd/ceapi.go | 5 +++++ internal/cmd/workspaces.go | 21 +++++++++++++-------- internal/cmd/workspaces_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/internal/cmd/ceapi.go b/internal/cmd/ceapi.go index c58b8161..0c5ca004 100644 --- a/internal/cmd/ceapi.go +++ b/internal/cmd/ceapi.go @@ -32,6 +32,11 @@ func lookupUserOrgs(user *coder.User, orgs []coder.Organization) []coder.Organiz return userOrgs } +// getAllWorkspaces gets all workspaces for all users, on all providers. +func getAllWorkspaces(ctx context.Context, client coder.Client) ([]coder.Workspace, error) { + return client.Workspaces(ctx) +} + // getWorkspaces returns all workspaces for the user. func getWorkspaces(ctx context.Context, client coder.Client, email string) ([]coder.Workspace, error) { user, err := client.UserByEmail(ctx, email) diff --git a/internal/cmd/workspaces.go b/internal/cmd/workspaces.go index ff70b64d..f4dd7e0f 100644 --- a/internal/cmd/workspaces.go +++ b/internal/cmd/workspaces.go @@ -70,6 +70,7 @@ const ( func lsWorkspacesCommand() *cobra.Command { var ( + all bool outputFmt string user string provider string @@ -85,16 +86,19 @@ func lsWorkspacesCommand() *cobra.Command { if err != nil { return err } - workspaces, err := getWorkspaces(ctx, client, user) + + var workspaces []coder.Workspace + switch { + case all: + workspaces, err = getAllWorkspaces(ctx, client) + case provider != "": + workspaces, err = getWorkspacesByProvider(ctx, client, provider, user) + default: + workspaces, err = getWorkspaces(ctx, client, user) + } if err != nil { return err } - if provider != "" { - workspaces, err = getWorkspacesByProvider(ctx, client, provider, user) - if err != nil { - return err - } - } if len(workspaces) < 1 { clog.LogInfo("no workspaces found") workspaces = []coder.Workspace{} // ensures that json output still marshals @@ -124,6 +128,7 @@ func lsWorkspacesCommand() *cobra.Command { }, } + cmd.Flags().BoolVar(&all, "all", false, "Get workspaces for all users (admin only)") cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") cmd.Flags().StringVarP(&outputFmt, "output", "o", humanOutput, "human | json") cmd.Flags().StringVarP(&provider, "provider", "p", "", "Filter workspaces by a particular workspace provider name.") @@ -210,7 +215,7 @@ func (*wsPinger) logSuccess(timeStr, msg string) { fmt.Printf("%s: %s\n", color.New(color.Bold, color.FgGreen).Sprint(timeStr), msg) } -// Only return fatal errors +// Only return fatal errors. func (w *wsPinger) ping(ctx context.Context) error { ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) defer cancelFunc() diff --git a/internal/cmd/workspaces_test.go b/internal/cmd/workspaces_test.go index 5dd3d01b..89e5fd08 100644 --- a/internal/cmd/workspaces_test.go +++ b/internal/cmd/workspaces_test.go @@ -29,6 +29,34 @@ func Test_workspaces_ls(t *testing.T) { res.stdoutUnmarshals(t, &workspaces) } +func Test_workspaces_ls_all(t *testing.T) { + skipIfNoAuth(t) + for _, test := range []struct { + name string + command []string + assert func(r result) + }{ + { + name: "simple list", + command: []string{"workspaces", "ls", "--all"}, + assert: func(r result) { r.success(t) }, + }, + { + name: "list as json", + command: []string{"workspaces", "ls", "--all", "--output", "json"}, + assert: func(r result) { + var workspaces []coder.Workspace + r.stdoutUnmarshals(t, &workspaces) + }, + }, + } { + test := test + t.Run(test.name, func(t *testing.T) { + test.assert(execute(t, nil, test.command...)) + }) + } +} + func Test_workspaces_ls_by_provider(t *testing.T) { skipIfNoAuth(t) for _, test := range []struct { From 59cae56aa324d525538004e50cd5961e8731a60f Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 13 Aug 2021 12:46:41 +1000 Subject: [PATCH 093/128] Fix docs (#418) --- docs/coder_workspaces_ls.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/coder_workspaces_ls.md b/docs/coder_workspaces_ls.md index cd940e2b..71ee02d9 100644 --- a/docs/coder_workspaces_ls.md +++ b/docs/coder_workspaces_ls.md @@ -13,6 +13,7 @@ coder workspaces ls [flags] ### Options ``` + --all Get workspaces for all users (admin only) -h, --help help for ls -o, --output string human | json (default "human") -p, --provider string Filter workspaces by a particular workspace provider name. From 4117dc819eb928a6bb7fd2268f66a37389a23f2f Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Fri, 13 Aug 2021 19:02:58 -0500 Subject: [PATCH 094/128] feat: add additional config methods to coder sdk (#419) --- coder-sdk/README.md | 5 +- coder-sdk/client.go | 24 +++++----- coder-sdk/config.go | 93 +++++++++++++++++++++++++++++++++----- internal/cmd/users_test.go | 4 +- 4 files changed, 97 insertions(+), 29 deletions(-) diff --git a/coder-sdk/README.md b/coder-sdk/README.md index 75ffd8dd..b5fbec97 100644 --- a/coder-sdk/README.md +++ b/coder-sdk/README.md @@ -1,7 +1,8 @@ # coder-sdk -`coder-sdk` is a Go client library for [Coder](https://coder.com). -It is not yet stable and therefore we do not recommend depending on the current state of its public APIs. +`coder-sdk` is a Go client library for [Coder](https://coder.com). +It is not yet stable and therefore we do not recommend depending on the current +state of its public APIs. ## Usage diff --git a/coder-sdk/client.go b/coder-sdk/client.go index 6d172126..1cba0fcb 100644 --- a/coder-sdk/client.go +++ b/coder-sdk/client.go @@ -28,27 +28,25 @@ type ClientOptions struct { // Token is the API Token used to authenticate (optional). // - // If Token is provided, the DefaultClient will use it to - // authenticate. If it is not provided, the client requires - // another type of credential, such as an Email/Password pair. + // If Token is provided, the DefaultClient will use it to authenticate. + // If it is not provided, the client requires another type of + // credential, such as an Email/Password pair. Token string // Email used to authenticate with Coder. // - // If you supply an Email and Password pair, NewClient will - // exchange these credentials for a Token during initialization. - // This is only applicable for the built-in authentication - // provider. The client will not retain these credentials in - // memory after NewClient returns. + // If you supply an Email and Password pair, NewClient will exchange + // these credentials for a Token during initialization. This is only + // applicable for the built-in authentication provider. The client will + // not retain these credentials in memory after NewClient returns. Email string // Password used to authenticate with Coder. // - // If you supply an Email and Password pair, NewClient will - // exchange these credentials for a Token during initialization. - // This is only applicable for the built-in authentication - // provider. The client will not retain these credentials in - // memory after NewClient returns. + // If you supply an Email and Password pair, NewClient will exchange + // these credentials for a Token during initialization. This is only + // applicable for the built-in authentication provider. The client will + // not retain these credentials in memory after NewClient returns. Password string } diff --git a/coder-sdk/config.go b/coder-sdk/config.go index c43ddf2c..a38f2357 100644 --- a/coder-sdk/config.go +++ b/coder-sdk/config.go @@ -15,14 +15,16 @@ const ( AuthProviderOIDC AuthProviderType = "oidc" ) -// ConfigAuth describes the authentication configuration for a Coder deployment. +// ConfigAuth describes the authentication configuration for a Coder +// deployment. type ConfigAuth struct { ProviderType *AuthProviderType `json:"provider_type"` OIDC *ConfigOIDC `json:"oidc"` SAML *ConfigSAML `json:"saml"` } -// ConfigOIDC describes the OIDC configuration for single-signon support in Coder. +// ConfigOIDC describes the OIDC configuration for single-signon support in +// Coder. type ConfigOIDC struct { ClientID *string `json:"client_id"` ClientSecret *string `json:"client_secret"` @@ -38,26 +40,30 @@ type ConfigSAML struct { PublicKeyCertificate *string `json:"public_key_certificate"` } -// ConfigOAuthBitbucketServer describes the Bitbucket integration configuration for a Coder deployment. +// ConfigOAuthBitbucketServer describes the Bitbucket integration configuration +// for a Coder deployment. type ConfigOAuthBitbucketServer struct { BaseURL string `json:"base_url" diff:"oauth.bitbucket_server.base_url"` } -// ConfigOAuthGitHub describes the Github integration configuration for a Coder deployment. +// ConfigOAuthGitHub describes the Github integration configuration for a Coder +// deployment. type ConfigOAuthGitHub struct { BaseURL string `json:"base_url"` ClientID string `json:"client_id"` ClientSecret string `json:"client_secret"` } -// ConfigOAuthGitLab describes the GitLab integration configuration for a Coder deployment. +// ConfigOAuthGitLab describes the GitLab integration configuration for a Coder +// deployment. type ConfigOAuthGitLab struct { BaseURL string `json:"base_url"` ClientID string `json:"client_id" ` ClientSecret string `json:"client_secret"` } -// ConfigOAuth describes the aggregate git integration configuration for a Coder deployment. +// ConfigOAuth describes the aggregate git integration configuration for a +// Coder deployment. type ConfigOAuth struct { BitbucketServer ConfigOAuthBitbucketServer `json:"bitbucket_server"` GitHub ConfigOAuthGitHub `json:"github"` @@ -140,18 +146,81 @@ func (c *DefaultClient) PutSiteConfigExtensionMarketplace(ctx context.Context, r // ConfigWorkspaces is the site configuration for workspace attributes. type ConfigWorkspaces struct { - GPUVendor string `json:"gpu_vendor,omitempty" valid:"in(nvidia|amd)"` - EnableContainerVMs bool `json:"enable_container_vms,omitempty"` - EnableWorkspacesAsCode bool `json:"enable_workspaces_as_code,omitempty"` - EnableP2P bool `json:"enable_p2p,omitempty"` + GPUVendor string `json:"gpu_vendor"` + EnableContainerVMs bool `json:"enable_container_vms"` + EnableWorkspacesAsCode bool `json:"enable_workspaces_as_code"` } // SiteConfigWorkspaces fetches the workspace configuration. func (c *DefaultClient) SiteConfigWorkspaces(ctx context.Context) (*ConfigWorkspaces, error) { var conf ConfigWorkspaces - // TODO: use the `/api/v0/workspaces/config route once we migrate from using general config - if err := c.requestBody(ctx, http.MethodGet, "/api/private/config", nil, &conf); err != nil { + if err := c.requestBody(ctx, http.MethodGet, "/api/v0/workspaces/config", nil, &conf); err != nil { return nil, err } return &conf, nil } + +// PutSiteConfigWorkspaces sets the workspace configuration. +func (c *DefaultClient) PutSiteConfigWorkspaces(ctx context.Context, req ConfigWorkspaces) error { + return c.requestBody(ctx, http.MethodPut, "/api/v0/workspaces/config", req, nil) +} + +type ConfigDormancy struct { + // UserDormancyThresholdDays is not currently updatable. + // UserDormancyThresholdDays int `json:"user_dormancy_threshold_days"` + UserDeletionThresholdDays int `json:"user_deletion_threshold_days"` +} + +// SiteConfigDormancy fetches the dormancy configuration. +func (c *DefaultClient) SiteConfigDormancy(ctx context.Context) (*ConfigDormancy, error) { + var conf ConfigDormancy + if err := c.requestBody(ctx, http.MethodGet, "/api/private/dormancy/config", nil, &conf); err != nil { + return nil, err + } + return &conf, nil +} + +// PutSiteConfigDormancy sets the dormancy configuration. +func (c *DefaultClient) PutSiteConfigDormancy(ctx context.Context, req ConfigDormancy) error { + return c.requestBody(ctx, http.MethodPut, "/api/private/dormancy/config", req, nil) +} + +type ConfigDevURLAccess struct { + Private bool `json:"private"` + Org bool `json:"org"` + Authed bool `json:"authed"` + Public bool `json:"public"` +} + +// SiteConfigDevURLAccess fetches the DevURL access configuration. +func (c *DefaultClient) SiteConfigDevURLAccess(ctx context.Context) (*ConfigDevURLAccess, error) { + var conf ConfigDevURLAccess + if err := c.requestBody(ctx, http.MethodGet, "/api/private/devurls/config", nil, &conf); err != nil { + return nil, err + } + return &conf, nil +} + +// PutSiteConfigDevURLAccess sets the DevURL access configuration. +func (c *DefaultClient) PutSiteConfigDevURLAccess(ctx context.Context, req ConfigDevURLAccess) error { + return c.requestBody(ctx, http.MethodPut, "/api/private/devurls/config", req, nil) +} + +// ConfigSSHSettings is the site configuration for SSH. +type ConfigSSHSettings struct { + KeygenAlgorithm string `json:"keygen_algorithm"` +} + +// SiteConfigSSHSettings fetches the SSH configuration. +func (c *DefaultClient) SiteConfigSSHSettings(ctx context.Context) (*ConfigSSHSettings, error) { + var conf ConfigSSHSettings + if err := c.requestBody(ctx, http.MethodGet, "/api/private/ssh/config", nil, &conf); err != nil { + return nil, err + } + return &conf, nil +} + +// PutSiteConfigSSHSettings sets the SSH configuration. +func (c *DefaultClient) PutSiteConfigSSHSettings(ctx context.Context, req ConfigSSHSettings) error { + return c.requestBody(ctx, http.MethodPut, "/api/private/ssh/config", req, nil) +} diff --git a/internal/cmd/users_test.go b/internal/cmd/users_test.go index a82f4607..dad57ed9 100644 --- a/internal/cmd/users_test.go +++ b/internal/cmd/users_test.go @@ -24,9 +24,9 @@ func Test_users(t *testing.T) { func assertAdmin(t *testing.T, users []coder.User) { for _, u := range users { - if u.Username == "admin" { + if u.Username == "kyle" { return } } - slogtest.Fatal(t, "did not find admin user", slog.F("users", users)) + slogtest.Fatal(t, "did not find kyle user", slog.F("users", users)) } From 59a0a209646960bfc3fbd7576a9513b9b76dfd8e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 17 Aug 2021 12:14:31 +0100 Subject: [PATCH 095/128] feat: Add update command to coder-cli (#417) * feat: Add update command to coder-cli This commit adds a new update subcommand that queries a Coder instance for its current version, fetches the corresponding version from GitHub releases if required, and updates the binary in-place. --- docs/coder.md | 1 + docs/coder_update.md | 31 ++ go.mod | 2 + go.sum | 3 + internal/cmd/cmd.go | 1 + internal/cmd/update.go | 490 ++++++++++++++++++++++++++++++++ internal/cmd/update_test.go | 545 ++++++++++++++++++++++++++++++++++++ internal/version/version.go | 2 +- 8 files changed, 1074 insertions(+), 1 deletion(-) create mode 100644 docs/coder_update.md create mode 100644 internal/cmd/update.go create mode 100644 internal/cmd/update_test.go diff --git a/docs/coder.md b/docs/coder.md index 17e7fa7f..513efb42 100644 --- a/docs/coder.md +++ b/docs/coder.md @@ -20,6 +20,7 @@ coder provides a CLI for working with an existing Coder installation * [coder ssh](coder_ssh.md) - Enter a shell of execute a command over SSH into a Coder workspace * [coder sync](coder_sync.md) - Establish a one way directory sync to a Coder workspace * [coder tokens](coder_tokens.md) - manage Coder API tokens for the active user +* [coder update](coder_update.md) - Update coder binary * [coder urls](coder_urls.md) - Interact with workspace DevURLs * [coder users](coder_users.md) - Interact with Coder user accounts * [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces diff --git a/docs/coder_update.md b/docs/coder_update.md new file mode 100644 index 00000000..8bcc9fae --- /dev/null +++ b/docs/coder_update.md @@ -0,0 +1,31 @@ +## coder update + +Update coder binary + +### Synopsis + +Update coder to the version matching a given coder instance. + +``` +coder update [flags] +``` + +### Options + +``` + --coder string query this coder instance for the matching version + --force do not prompt for confirmation + -h, --help help for update + --version string explicitly specify which version to fetch and install +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation + diff --git a/go.mod b/go.mod index db32d750..e67ff7f2 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.14 require ( cdr.dev/slog v1.4.1 cdr.dev/wsep v0.0.0-20200728013649-82316a09813f + github.com/Masterminds/semver/v3 v3.1.1 github.com/briandowns/spinner v1.16.0 github.com/cli/safeexec v1.0.0 github.com/fatih/color v1.12.0 @@ -23,6 +24,7 @@ require ( github.com/pion/webrtc/v3 v3.0.32 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 + github.com/spf13/afero v1.6.0 github.com/spf13/cobra v1.2.1 github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 diff --git a/go.sum b/go.sum index 755babf6..0435e11b 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= @@ -378,6 +380,7 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 26df6bc4..90911c07 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -38,6 +38,7 @@ func Make() *cobra.Command { tagsCmd(), tokensCmd(), tunnelCmd(), + updateCmd(), urlCmd(), usersCmd(), workspacesCmd(), diff --git a/internal/cmd/update.go b/internal/cmd/update.go new file mode 100644 index 00000000..015b807e --- /dev/null +++ b/internal/cmd/update.go @@ -0,0 +1,490 @@ +package cmd + +import ( + "archive/tar" + "archive/zip" + "bufio" + "bytes" + "compress/gzip" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" + "time" + + "golang.org/x/xerrors" + + "cdr.dev/coder-cli/internal/config" + "cdr.dev/coder-cli/internal/version" + "cdr.dev/coder-cli/pkg/clog" + + "github.com/Masterminds/semver/v3" + "github.com/manifoldco/promptui" + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +const ( + goosWindows = "windows" + goosLinux = "linux" + apiPrivateVersion = "/api/private/version" +) + +// updater updates coder-cli. +type updater struct { + confirmF func(string) (string, error) + execF func(context.Context, string, ...string) ([]byte, error) + executablePath string + fs afero.Fs + httpClient getter + osF func() string + versionF func() string +} + +func updateCmd() *cobra.Command { + var ( + force bool + coderURL string + versionArg string + ) + + cmd := &cobra.Command{ + Use: "update", + Short: "Update coder binary", + Long: "Update coder to the version matching a given coder instance.", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + httpClient := &http.Client{ + Timeout: 10 * time.Second, + } + + currExe, err := os.Executable() + if err != nil { + return clog.Fatal("init: get current executable", clog.Causef(err.Error())) + } + + updater := &updater{ + confirmF: defaultConfirm, + execF: defaultExec, + executablePath: currExe, + httpClient: httpClient, + fs: afero.NewOsFs(), + osF: func() string { return runtime.GOOS }, + versionF: func() string { return version.Version }, + } + return updater.Run(ctx, force, coderURL, versionArg) + }, + } + + cmd.Flags().BoolVar(&force, "force", false, "do not prompt for confirmation") + cmd.Flags().StringVar(&coderURL, "coder", "", "query this coder instance for the matching version") + cmd.Flags().StringVar(&versionArg, "version", "", "explicitly specify which version to fetch and install") + + return cmd +} + +type getter interface { + Get(url string) (*http.Response, error) +} + +func (u *updater) Run(ctx context.Context, force bool, coderURLArg string, versionArg string) error { + // Check under following directories and warn if coder binary is under them: + // * C:\Windows\ + // * homebrew prefix + // * coder assets root (/var/tmp/coder) + var pathBlockList = []string{ + `C:\Windows\`, + `/var/tmp/coder`, + } + brewPrefixCmd, err := u.execF(ctx, "brew", "--prefix") + if err == nil { // ignore errors if homebrew not installed + pathBlockList = append(pathBlockList, strings.TrimSpace(string(brewPrefixCmd))) + } + + for _, prefix := range pathBlockList { + if HasFilePathPrefix(u.executablePath, prefix) { + return clog.Fatal( + "cowardly refusing to update coder binary", + clog.BlankLine, + clog.Causef("executable path %q is under blocklisted prefix %q", u.executablePath, prefix)) + } + } + + currentBinaryStat, err := u.fs.Stat(u.executablePath) + if err != nil { + return clog.Fatal("preflight: cannot stat current binary", clog.Causef(err.Error())) + } + + if currentBinaryStat.Mode().Perm()&0222 == 0 { + return clog.Fatal("preflight: missing write permission on current binary") + } + + clog.LogInfo(fmt.Sprintf("Current version of coder-cli is %s", version.Version)) + + desiredVersion, err := getDesiredVersion(u.httpClient, coderURLArg, versionArg) + if err != nil { + return clog.Fatal("failed to determine desired version of coder", clog.Causef(err.Error())) + } + + currentVersion, err := semver.StrictNewVersion(u.versionF()) + if err != nil { + clog.LogWarn("failed to determine current version of coder-cli", clog.Causef(err.Error())) + } else if currentVersion.Compare(desiredVersion) == 0 { + clog.LogInfo("Up to date!") + return nil + } + + if !force { + label := fmt.Sprintf("Do you want to download version %s instead", desiredVersion) + if _, err := u.confirmF(label); err != nil { + return clog.Fatal("user cancelled operation", clog.Tipf(`use "--force" to update without confirmation`)) + } + } + + downloadURL, err := queryGithubAssetURL(u.httpClient, desiredVersion, u.osF()) + if err != nil { + return clog.Fatal("failed to query github assets url", clog.Causef(err.Error())) + } + + var downloadBuf bytes.Buffer + memWriter := bufio.NewWriter(&downloadBuf) + + clog.LogInfo("fetching coder-cli from GitHub releases", downloadURL) + resp, err := u.httpClient.Get(downloadURL) + if err != nil { + return clog.Fatal(fmt.Sprintf("failed to fetch URL %s", downloadURL), clog.Causef(err.Error())) + } + + if resp.StatusCode != http.StatusOK { + return clog.Fatal("failed to fetch release", clog.Causef("URL %s returned status code %d", downloadURL, resp.StatusCode)) + } + + if _, err := io.Copy(memWriter, resp.Body); err != nil { + return clog.Fatal(fmt.Sprintf("failed to download %s", downloadURL), clog.Causef(err.Error())) + } + + _ = resp.Body.Close() + + if err := memWriter.Flush(); err != nil { + return clog.Fatal(fmt.Sprintf("failed to save %s", downloadURL), clog.Causef(err.Error())) + } + + // TODO: validate the checksum of the downloaded file. GitHub does not currently provide this information + // and we do not generate them yet. + var updatedBinaryName string + if u.osF() == "windows" { + updatedBinaryName = "coder.exe" + } else { + updatedBinaryName = "coder" + } + updatedBinary, err := extractFromArchive(updatedBinaryName, downloadBuf.Bytes()) + if err != nil { + return clog.Fatal("failed to extract coder binary from archive", clog.Causef(err.Error())) + } + + // We assume the binary is named coder and write it to coder.new + updatedCoderBinaryPath := u.executablePath + ".new" + updatedBin, err := u.fs.OpenFile(updatedCoderBinaryPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, currentBinaryStat.Mode().Perm()) + if err != nil { + return clog.Fatal("failed to create file for updated coder binary", clog.Causef(err.Error())) + } + + fsWriter := bufio.NewWriter(updatedBin) + if _, err := io.Copy(fsWriter, bytes.NewReader(updatedBinary)); err != nil { + return clog.Fatal("failed to write updated coder binary to disk", clog.Causef(err.Error())) + } + + if err := fsWriter.Flush(); err != nil { + return clog.Fatal("failed to persist updated coder binary to disk", clog.Causef(err.Error())) + } + + _ = updatedBin.Close() + + if err := u.doUpdate(ctx, updatedCoderBinaryPath); err != nil { + return clog.Fatal("failed to update coder binary", clog.Causef(err.Error())) + } + + clog.LogSuccess("Updated coder CLI to version " + desiredVersion.String()) + return nil +} + +func (u *updater) doUpdate(ctx context.Context, updatedCoderBinaryPath string) error { + var err error + // TODO(cian): on Windows, we must do two things differently: + // 1) Calling the updated binary fails due to the xterminal.MakeOutputRaw call in main; skipping this check on Windows. + // 2) We must rename the currently running binary before renaming the new binary + if u.osF() == goosWindows { + err = u.fs.Rename(u.executablePath, updatedCoderBinaryPath+".old") + if err != nil { + return xerrors.Errorf("windows: rename current coder binary: %w", err) + } + err = u.fs.Rename(updatedCoderBinaryPath, u.executablePath) + if err != nil { + return xerrors.Errorf("windows: rename updated coder binary: %w", err) + } + return nil + } + + // validate that we can execute the new binary before overwriting + updatedVersionOutput, err := u.execF(ctx, updatedCoderBinaryPath, "--version") + if err != nil { + return xerrors.Errorf("check version of updated coder binary: %w", err) + } + clog.LogInfo(fmt.Sprintf("updated binary reports %s", bytes.TrimSpace(updatedVersionOutput))) + + if err = u.fs.Rename(updatedCoderBinaryPath, u.executablePath); err != nil { + return xerrors.Errorf("update coder binary in-place: %w", err) + } + + return nil +} + +func getDesiredVersion(httpClient getter, coderURLArg string, versionArg string) (*semver.Version, error) { + var coderURL *url.URL + var desiredVersion *semver.Version + var err error + + if coderURLArg != "" && versionArg != "" { + clog.LogWarn(fmt.Sprintf("ignoring the version reported by %q", coderURLArg), clog.Causef("--version flag was specified explicitly")) + } + + if versionArg != "" { + desiredVersion, err = semver.StrictNewVersion(versionArg) + if err != nil { + return &semver.Version{}, xerrors.Errorf("parse desired version arg: %w", err) + } + return desiredVersion, nil + } + + if coderURLArg == "" { + coderURL, err = getCoderConfigURL() + if err != nil { + return &semver.Version{}, xerrors.Errorf("get coder url: %w", err) + } + } else { + coderURL, err = url.Parse(coderURLArg) + if err != nil { + return &semver.Version{}, xerrors.Errorf("parse coder url arg: %w", err) + } + } + + desiredVersion, err = getAPIVersionUnauthed(httpClient, *coderURL) + if err != nil { + return &semver.Version{}, xerrors.Errorf("query coder version: %w", err) + } + + clog.LogInfo(fmt.Sprintf("Coder instance at %q reports version %s", coderURL.String(), desiredVersion.String())) + + return desiredVersion, nil +} + +func defaultConfirm(label string) (string, error) { + p := promptui.Prompt{IsConfirm: true, Label: label} + return p.Run() +} + +func queryGithubAssetURL(httpClient getter, version *semver.Version, ostype string) (string, error) { + var b bytes.Buffer + fmt.Fprintf(&b, "%d", version.Major()) + fmt.Fprint(&b, ".") + fmt.Fprintf(&b, "%d", version.Minor()) + fmt.Fprint(&b, ".") + fmt.Fprintf(&b, "%d", version.Patch()) + if version.Prerelease() != "" { + fmt.Fprint(&b, "-") + fmt.Fprint(&b, version.Prerelease()) + } + + urlString := fmt.Sprintf("https://api.github.com/repos/cdr/coder-cli/releases/tags/v%s", b.String()) + clog.LogInfo("query github releases", fmt.Sprintf("url: %q", urlString)) + + type asset struct { + BrowserDownloadURL string `json:"browser_download_url"` + Name string `json:"name"` + } + type release struct { + Assets []asset `json:"assets"` + } + var r release + + resp, err := httpClient.Get(urlString) + if err != nil { + return "", xerrors.Errorf("query github release url %s: %w", urlString, err) + } + defer resp.Body.Close() + + err = json.NewDecoder(resp.Body).Decode(&r) + if err != nil { + return "", xerrors.Errorf("unmarshal github releases api response: %w", err) + } + + var assetURLStr string + for _, a := range r.Assets { + if strings.HasPrefix(a.Name, "coder-cli-"+ostype) { + assetURLStr = a.BrowserDownloadURL + } + } + + if assetURLStr == "" { + return "", xerrors.Errorf("could not find release for ostype %s", ostype) + } + + return assetURLStr, nil +} + +func extractFromArchive(path string, archive []byte) ([]byte, error) { + contentType := http.DetectContentType(archive) + switch contentType { + case "application/zip": + return extractFromZip(path, archive) + case "application/x-gzip": + return extractFromTgz(path, archive) + default: + return nil, xerrors.Errorf("unknown archive type: %s", contentType) + } +} + +func extractFromZip(path string, archive []byte) ([]byte, error) { + zipReader, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive))) + if err != nil { + return nil, xerrors.Errorf("failed to open zip archive") + } + + var zf *zip.File + for _, f := range zipReader.File { + if f.Name == path { + zf = f + break + } + } + if zf == nil { + return nil, xerrors.Errorf("could not find path %q in zip archive", path) + } + + rc, err := zf.Open() + if err != nil { + return nil, xerrors.Errorf("failed to extract path %q from archive", path) + } + defer rc.Close() + + var b bytes.Buffer + bw := bufio.NewWriter(&b) + if _, err := io.Copy(bw, rc); err != nil { + return nil, xerrors.Errorf("failed to copy path %q to from archive", path) + } + return b.Bytes(), nil +} + +func extractFromTgz(path string, archive []byte) ([]byte, error) { + zr, err := gzip.NewReader(bytes.NewReader(archive)) + if err != nil { + return nil, xerrors.Errorf("failed to gunzip archive") + } + + tr := tar.NewReader(zr) + + var b bytes.Buffer + bw := bufio.NewWriter(&b) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, xerrors.Errorf("failed to read tar archive: %w", err) + } + fi := hdr.FileInfo() + if fi.Name() == path && fi.Mode().IsRegular() { + _, err = io.Copy(bw, tr) + if err != nil { + return nil, xerrors.Errorf("failed to read file %q from archive", fi.Name()) + } + break + } + } + + return b.Bytes(), nil +} + +// getCoderConfigURL reads the currently configured coder URL, returning an empty string if not configured. +func getCoderConfigURL() (*url.URL, error) { + urlString, err := config.URL.Read() + if err != nil { + return nil, err + } + configuredURL, err := url.Parse(strings.TrimSpace(urlString)) + if err != nil { + return nil, err + } + return configuredURL, nil +} + +// XXX: coder.Client requires an API key, but we may not be logged into the coder instance for which we +// want to determine the version. We don't need an API key to hit /api/private/version though. +func getAPIVersionUnauthed(client getter, baseURL url.URL) (*semver.Version, error) { + baseURL.Path = path.Join(baseURL.Path, "/api/private/version") + resp, err := client.Get(baseURL.String()) + if err != nil { + return nil, xerrors.Errorf("get %s: %w", baseURL.String(), err) + } + defer resp.Body.Close() + + ver := struct { + Version string `json:"version"` + }{} + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, xerrors.Errorf("read response body: %w", err) + } + + if err := json.Unmarshal(body, &ver); err != nil { + return nil, xerrors.Errorf("parse version response: %w", err) + } + + version, err := semver.StrictNewVersion(ver.Version) + if err != nil { + return nil, xerrors.Errorf("parsing coder version: %w", err) + } + + return version, nil +} + +// HasFilePathPrefix reports whether the filesystem path s +// begins with the elements in prefix. +// Lifted from github.com/golang/go/blob/master/src/cmd/internal/str/path.go. +func HasFilePathPrefix(s, prefix string) bool { + sv := strings.ToUpper(filepath.VolumeName(s)) + pv := strings.ToUpper(filepath.VolumeName(prefix)) + s = s[len(sv):] + prefix = prefix[len(pv):] + switch { + default: + return false + case sv != pv: + return false + case len(s) == len(prefix): + return s == prefix + case prefix == "": + return true + case len(s) > len(prefix): + if prefix[len(prefix)-1] == filepath.Separator { + return strings.HasPrefix(s, prefix) + } + return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix + } +} + +// defaultExec wraps exec.CommandContext. +func defaultExec(ctx context.Context, cmd string, args ...string) ([]byte, error) { + return exec.CommandContext(ctx, cmd, args...).CombinedOutput() +} diff --git a/internal/cmd/update_test.go b/internal/cmd/update_test.go new file mode 100644 index 00000000..c751e2c4 --- /dev/null +++ b/internal/cmd/update_test.go @@ -0,0 +1,545 @@ +package cmd + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "io/fs" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "cdr.dev/slog/sloggers/slogtest/assert" + "github.com/manifoldco/promptui" + "github.com/spf13/afero" + "golang.org/x/xerrors" + + "cdr.dev/coder-cli/pkg/clog" +) + +const ( + fakeExePathLinux = "/home/user/bin/coder" + fakeExePathWindows = `C:\Users\user\bin\coder.exe` + fakeCoderURL = "https://my.cdr.dev" + fakeNewVersion = "1.23.4-rc.5+678-gabcdef-12345678" + fakeOldVersion = "1.22.4-rc.5+678-gabcdef-12345678" + filenameLinux = "coder-cli-linux-amd64.tar.gz" + filenameWindows = "coder-cli-windows.zip" + fakeGithubReleaseURL = "https://api.github.com/repos/cdr/coder-cli/releases/tags/v1.23.4-rc.5" +) + +var ( + apiPrivateVersionURL = fakeCoderURL + apiPrivateVersion + fakeError = xerrors.New("fake error for testing") + fakeNewVersionJSON = fmt.Sprintf(`{"version":%q}`, fakeNewVersion) + fakeOldVersionJSON = fmt.Sprintf(`{"version":%q}`, fakeOldVersion) + fakeAssetURLLinux = "https://github.com/cdr/coder-cli/releases/download/v1.23.4-rc.5/" + filenameLinux + fakeAssetURLWindows = "https://github.com/cdr/coder-cli/releases/download/v1.23.4-rc.5/" + filenameWindows + fakeGithubReleaseJSON = fmt.Sprintf(`{"assets":[{"name":%q,"browser_download_url":%q},{"name":%q,"browser_download_url":%q}]}`, filenameLinux, fakeAssetURLLinux, filenameWindows, fakeAssetURLWindows) +) + +func Test_updater_run(t *testing.T) { + t.Parallel() + + // params holds parameters for each test case + type params struct { + ConfirmF func(string) (string, error) + Ctx context.Context + Execer *fakeExecer + ExecutablePath string + Fakefs afero.Fs + HTTPClient *fakeGetter + OsF func() string + VersionF func() string + } + + // fromParams creates a new updater from params + fromParams := func(p *params) *updater { + return &updater{ + confirmF: p.ConfirmF, + execF: p.Execer.ExecF, + executablePath: p.ExecutablePath, + fs: p.Fakefs, + httpClient: p.HTTPClient, + osF: p.OsF, + versionF: p.VersionF, + } + } + + run := func(t *testing.T, name string, fn func(t *testing.T, p *params)) { + t.Run(name, func(t *testing.T) { + t.Logf("running %s", name) + ctx := context.Background() + fakefs := afero.NewMemMapFs() + execer := newFakeExecer(t) + execer.M["brew --prefix"] = fakeExecerResult{[]byte{}, os.ErrNotExist} + params := ¶ms{ + // This must be overridden inside run() + ConfirmF: func(string) (string, error) { + t.Errorf("unhandled ConfirmF") + t.FailNow() + return "", nil + }, + Execer: execer, + Ctx: ctx, + ExecutablePath: fakeExePathLinux, + Fakefs: fakefs, + HTTPClient: newFakeGetter(t), + // Default to GOOS=linux + OsF: func() string { return goosLinux }, + // This must be overridden inside run() + VersionF: func() string { + t.Errorf("unhandled VersionF") + t.FailNow() + return "" + }, + } + + fn(t, params) + }) + } + + run(t, "update coder - noop", func(t *testing.T, p *params) { + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeNewVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) + p.VersionF = func() string { return fakeNewVersion } + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assert.Success(t, "update coder - noop", err) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) + }) + + run(t, "update coder - explicit version specified", func(t *testing.T, p *params) { + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeOldVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.VersionF = func() string { return fakeOldVersion } + p.ConfirmF = fakeConfirmYes + p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, fakeNewVersion) + assert.Success(t, "update coder - explicit version specified", err) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) + }) + + run(t, "update coder - old to new", func(t *testing.T, p *params) { + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.VersionF = func() string { return fakeOldVersion } + p.ConfirmF = fakeConfirmYes + p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assert.Success(t, "update coder - old to new", err) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) + }) + + run(t, "update coder - old to new - binary renamed", func(t *testing.T, p *params) { + p.ExecutablePath = "/home/user/bin/coder-cli" + fakeFile(t, p.Fakefs, p.ExecutablePath, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.VersionF = func() string { return fakeOldVersion } + p.ConfirmF = fakeConfirmYes + p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} + u := fromParams(p) + assertFileContent(t, p.Fakefs, p.ExecutablePath, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assert.Success(t, "update coder - old to new - binary renamed", err) + assertFileContent(t, p.Fakefs, p.ExecutablePath, fakeNewVersion) + }) + + run(t, "update coder - old to new - windows", func(t *testing.T, p *params) { + p.OsF = func() string { return goosWindows } + p.ExecutablePath = fakeExePathWindows + fakeFile(t, p.Fakefs, fakeExePathWindows, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLWindows] = newFakeGetterResponse(fakeValidZipBytes, 200, variadicS(), nil) + p.VersionF = func() string { return fakeOldVersion } + p.ConfirmF = fakeConfirmYes + p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathWindows, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assert.Success(t, "update coder - old to new - windows", err) + assertFileContent(t, p.Fakefs, fakeExePathWindows, fakeNewVersion) + }) + + run(t, "update coder - old to new forced", func(t *testing.T, p *params) { + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.VersionF = func() string { return fakeOldVersion } + p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, true, fakeCoderURL, "") + assert.Success(t, "update coder - old to new forced", err) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) + }) + + run(t, "update coder - user cancelled", func(t *testing.T, p *params) { + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) + p.VersionF = func() string { return fakeOldVersion } + p.ConfirmF = fakeConfirmNo + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - user cancelled", err, "user cancelled operation", "") + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + }) + + run(t, "update coder - cannot stat", func(t *testing.T, p *params) { + u := fromParams(p) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - cannot stat", err, "cannot stat current binary", os.ErrNotExist.Error()) + }) + + run(t, "update coder - no permission", func(t *testing.T, p *params) { + fakeFile(t, p.Fakefs, fakeExePathLinux, 0400, fakeOldVersion) + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - no permission", err, "missing write permission", "") + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + }) + + run(t, "update coder - invalid version arg", func(t *testing.T, p *params) { + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) + p.VersionF = func() string { return fakeOldVersion } + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "Invalid Semantic Version") + assertCLIError(t, "update coder - invalid version arg", err, "failed to determine desired version of coder", "Invalid Semantic Version") + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + }) + + run(t, "update coder - invalid url", func(t *testing.T, p *params) { + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) + p.VersionF = func() string { return fakeOldVersion } + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, false, "h$$p://invalid.url", "") + assertCLIError(t, "update coder - invalid url", err, "failed to determine desired version of coder", "first path segment in URL cannot contain colon") + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + }) + + run(t, "update coder - fetch api version failure", func(t *testing.T, p *params) { + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte{}, 401, variadicS(), fakeError) + p.VersionF = func() string { return fakeOldVersion } + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - fetch api version failure", err, "failed to determine desired version of coder", fakeError.Error()) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + }) + + run(t, "update coder - failed to query github releases", func(t *testing.T, p *params) { + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte{}, 0, variadicS(), fakeError) + p.VersionF = func() string { return fakeOldVersion } + p.ConfirmF = fakeConfirmYes + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - failed to query github releases", err, "failed to query github assets", fakeError.Error()) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + }) + + run(t, "update coder - failed to fetch URL", func(t *testing.T, p *params) { + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse([]byte{}, 0, variadicS(), fakeError) + p.VersionF = func() string { return fakeOldVersion } + p.ConfirmF = fakeConfirmYes + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - failed to fetch URL", err, "failed to fetch URL", fakeError.Error()) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + }) + + run(t, "update coder - release URL 404", func(t *testing.T, p *params) { + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse([]byte{}, 404, variadicS(), nil) + p.VersionF = func() string { return fakeOldVersion } + p.ConfirmF = fakeConfirmYes + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - release URL 404", err, "failed to fetch release", "status code 404") + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + }) + + run(t, "update coder - invalid tgz archive", func(t *testing.T, p *params) { + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse([]byte{}, 200, variadicS(), nil) + p.VersionF = func() string { return fakeOldVersion } + p.ConfirmF = fakeConfirmYes + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - invalid tgz archive", err, "failed to extract coder binary from archive", "unknown archive type") + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + }) + + run(t, "update coder - invalid zip archive", func(t *testing.T, p *params) { + p.OsF = func() string { return goosWindows } + p.ExecutablePath = fakeExePathWindows + fakeFile(t, p.Fakefs, fakeExePathWindows, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLWindows] = newFakeGetterResponse([]byte{}, 200, variadicS(), nil) + p.VersionF = func() string { return fakeOldVersion } + p.ConfirmF = fakeConfirmYes + u := fromParams(p) + assertFileContent(t, p.Fakefs, p.ExecutablePath, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - invalid zip archive", err, "failed to extract coder binary from archive", "unknown archive type") + assertFileContent(t, p.Fakefs, p.ExecutablePath, fakeOldVersion) + }) + + run(t, "update coder - read-only fs", func(t *testing.T, p *params) { + rwfs := p.Fakefs + p.Fakefs = afero.NewReadOnlyFs(rwfs) + fakeFile(t, rwfs, fakeExePathLinux, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.VersionF = func() string { return fakeOldVersion } + p.ConfirmF = fakeConfirmYes + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - read-only fs", err, "failed to create file", "") + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + }) + + run(t, "update coder - cannot exec new binary", func(t *testing.T, p *params) { + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.VersionF = func() string { return fakeOldVersion } + p.ConfirmF = fakeConfirmYes + p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{nil, fakeError} + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - cannot exec new binary", err, "failed to update coder binary", fakeError.Error()) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + }) + + if runtime.GOOS == goosWindows { + run(t, "update coder - path blocklist - windows", func(t *testing.T, p *params) { + p.ExecutablePath = `C:\Windows\system32\coder.exe` + u := fromParams(p) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - path blocklist - windows", err, "cowardly refusing to update coder binary", "blocklisted prefix") + }) + } else { + run(t, "update coder - path blocklist - coder assets dir", func(t *testing.T, p *params) { + p.ExecutablePath = `/var/tmp/coder/coder` + u := fromParams(p) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - path blocklist - windows", err, "cowardly refusing to update coder binary", "blocklisted prefix") + }) + run(t, "update coder - path blocklist - old homebrew prefix", func(t *testing.T, p *params) { + p.Execer.M["brew --prefix"] = fakeExecerResult{[]byte("/usr/local"), nil} + p.ExecutablePath = `/usr/local/bin/coder` + u := fromParams(p) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - path blocklist - old homebrew prefix", err, "cowardly refusing to update coder binary", "blocklisted prefix") + }) + run(t, "update coder - path blocklist - new homebrew prefix", func(t *testing.T, p *params) { + p.Execer.M["brew --prefix"] = fakeExecerResult{[]byte("/opt/homebrew"), nil} + p.ExecutablePath = `/opt/homebrew/bin/coder` + u := fromParams(p) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - path blocklist - new homebrew prefix", err, "cowardly refusing to update coder binary", "blocklisted prefix") + }) + run(t, "update coder - path blocklist - linuxbrew", func(t *testing.T, p *params) { + p.Execer.M["brew --prefix"] = fakeExecerResult{[]byte("/home/user/.linuxbrew"), nil} + p.ExecutablePath = `/home/user/.linuxbrew/bin/coder` + u := fromParams(p) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assertCLIError(t, "update coder - path blocklist - linuxbrew", err, "cowardly refusing to update coder binary", "blocklisted prefix") + }) + } +} + +// fakeGetter mocks HTTP requests. +type fakeGetter struct { + M map[string]*fakeGetterResponse + T *testing.T +} + +func newFakeGetter(t *testing.T) *fakeGetter { + return &fakeGetter{ + M: make(map[string]*fakeGetterResponse), + T: t, + } +} + +// Get returns the configured response for url. If no response configured, test fails immediately. +func (f *fakeGetter) Get(url string) (*http.Response, error) { + f.T.Helper() + val, ok := f.M[url] + if !ok { + f.T.Errorf("unhandled url: %s", url) + f.T.FailNow() + return nil, nil // this will never happen + } + return val.Resp, val.Err +} + +type fakeGetterResponse struct { + Resp *http.Response + Err error +} + +// newFakeGetterResponse is a convenience function for mocking HTTP requests. +func newFakeGetterResponse(body []byte, code int, headers []string, err error) *fakeGetterResponse { + resp := &http.Response{} + resp.Body = ioutil.NopCloser(bytes.NewReader(body)) + resp.StatusCode = code + resp.Header = http.Header{} + + for _, e := range headers { + parts := strings.Split(e, ":") + k := strings.ToLower(strings.TrimSpace(parts[0])) + v := strings.ToLower(strings.TrimSpace(strings.Join(parts[1:], ":"))) + resp.Header.Set(k, v) + } + + return &fakeGetterResponse{ + Resp: resp, + Err: err, + } +} + +func variadicS(s ...string) []string { + return s +} + +func fakeConfirmYes(_ string) (string, error) { + return "y", nil +} + +func fakeConfirmNo(_ string) (string, error) { + return "", promptui.ErrAbort +} + +func fakeFile(t *testing.T, fs afero.Fs, name string, perm fs.FileMode, content string) { + t.Helper() + err := fs.MkdirAll(filepath.Dir(name), 0750) + if err != nil { + panic(err) + } + f, err := fs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm) + if err != nil { + panic(err) + } + defer f.Close() + + _, err = f.Write([]byte(content)) + if err != nil { + panic(err) + } +} + +func assertFileContent(t *testing.T, fs afero.Fs, name string, content string) { + t.Helper() + f, err := fs.OpenFile(name, os.O_RDONLY, 0) + assert.Success(t, "open file "+name, err) + defer f.Close() + + b, err := ioutil.ReadAll(f) + assert.Success(t, "read file "+name, err) + + assert.Equal(t, "assert content equal", content, string(b)) +} + +func assertCLIError(t *testing.T, name string, err error, expectedHeader, expectedLines string) { + t.Helper() + cliError, ok := err.(clog.CLIError) + if !ok { + t.Errorf("%s: assert cli error: %+v is not a cli error", name, err) + } + + if !strings.Contains(err.Error(), expectedHeader) { + t.Errorf("%s: assert cli error: expected header %q to contain %q", name, err.Error(), expectedHeader) + } + + if expectedLines == "" { + return + } + + fullLines := strings.Join(cliError.Lines, "\n") + if !strings.Contains(fullLines, expectedLines) { + t.Errorf("%s: assert cli error: expected %q to contain %q", name, fullLines, expectedLines) + } +} + +// this is a valid tgz archive containing a single file named 'coder' with permissions 0751 +// containing the string "1.23.4-rc.5+678-gabcdef-12345678". +var fakeValidTgzBytes, _ = base64.StdEncoding.DecodeString(`H4sIAAAAAAAAA+3QsQ4CIRCEYR6F3oC7wIqvc3KnpQnq+3tGCwsTK3LN/zWTTDWZuG/XeeluJFlV +s1dqNfnOtyJOi4qllHOuTlSTqPMydNXH43afuvfu3w3jb9qExpRjCb1F2x3qMVymU5uXc9CUi63F +1vsAAAAAAAAAAAAAAAAAAL89AYuL424AKAAA`) + +// this is a valid zip archive containing a single file named 'coder.exe' with permissions 0751 +// containing the string "1.23.4-rc.5+678-gabcdef-12345678". +var fakeValidZipBytes, _ = base64.StdEncoding.DecodeString(`UEsDBAoAAAAAAAtfDVNCHNDCIAAAACAAAAAJABwAY29kZXIuZXhlVVQJAAPmXRZh/10WYXV4CwAB +BOgDAAAE6AMAADEuMjMuNC1yYy41KzY3OC1nYWJjZGVmLTEyMzQ1Njc4UEsBAh4DCgAAAAAAC18N +U0Ic0MIgAAAAIAAAAAkAGAAAAAAAAQAAAO2BAAAAAGNvZGVyLmV4ZVVUBQAD5l0WYXV4CwABBOgD +AAAE6AMAAFBLBQYAAAAAAQABAE8AAABjAAAAAAA=`) + +type fakeExecer struct { + M map[string]fakeExecerResult + T *testing.T +} + +func (f *fakeExecer) ExecF(_ context.Context, cmd string, args ...string) ([]byte, error) { + cmdAndArgs := strings.Join(append([]string{cmd}, args...), " ") + val, ok := f.M[cmdAndArgs] + if !ok { + f.T.Errorf("unhandled cmd %q", cmd) + f.T.FailNow() + return nil, nil // will never happen + } + return val.Output, val.Err +} + +func newFakeExecer(t *testing.T) *fakeExecer { + return &fakeExecer{ + M: make(map[string]fakeExecerResult), + T: t, + } +} + +type fakeExecerResult struct { + Output []byte + Err error +} diff --git a/internal/version/version.go b/internal/version/version.go index ce1d5de9..8873f158 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -1,5 +1,5 @@ // Package version contains the compile-time injected version string and -// related utiliy methods. +// related utility methods. package version import ( From a430ef66945de168c1798a20f82b810a9e1269ca Mon Sep 17 00:00:00 2001 From: Jonathan Yu Date: Mon, 23 Aug 2021 07:30:00 -0700 Subject: [PATCH 096/128] chore: update build settings (#420) * Update actions/checkout from v1 to @v2 * Configure build to run on pushes/pull requests on main * Add restrictive workflow permissions * Update minimum setup-go from 1.16.3 to 1.16.7 * Enable workflow dispatch for all builds --- .github/workflows/build.yaml | 45 ++++++++++++++++++++++++++---- .github/workflows/integration.yaml | 33 ++++++++++++++++++++-- .github/workflows/release.yaml | 40 ++++++++++++++++++++++++-- .github/workflows/test.yaml | 24 +++++++++++++++- 4 files changed, 130 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a0d76acf..33c5b836 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,44 +1,79 @@ name: build -on: [push] + +on: + push: + branches: + - main + + pull_request: + branches: + - main + + workflow_dispatch: + +permissions: + actions: none + checks: none + contents: read + deployments: none + issues: none + packages: none + pull-requests: none + repository-projects: none + security-events: none + statuses: none jobs: build: runs-on: ubuntu-20.04 steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-go@v2 with: - go-version: '^1.16.3' + go-version: '^1.16.7' + - name: Build run: make -j build/linux build/windows + - name: Upload uses: actions/upload-artifact@v2 with: name: coder-cli path: ./ci/bin/coder-cli-* + build_darwin: runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-go@v2 with: - go-version: '^1.16.3' + go-version: '^1.16.7' + - name: Install Gon run: | brew tap mitchellh/gon brew install mitchellh/gon/gon + - name: Import Signing Certificates uses: Apple-Actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} + - name: Build run: make build/macos env: AC_USERNAME: ${{ secrets.AC_USERNAME }} AC_PASSWORD: ${{ secrets.AC_PASSWORD }} + - name: Upload uses: actions/upload-artifact@v2 with: diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index e7abedaa..bb1a39d4 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -1,8 +1,30 @@ name: integration + on: push: + branches: + - main + + pull_request: + branches: + - main + schedule: - - cron: '*/180 * * * *' + - cron: '*/180 * * * *' + + workflow_dispatch: + +permissions: + actions: none + checks: none + contents: read + deployments: none + issues: none + packages: none + pull-requests: none + repository-projects: none + security-events: none + statuses: none jobs: integration: @@ -12,15 +34,20 @@ jobs: CODER_EMAIL: ${{ secrets.CODER_EMAIL }} CODER_PASSWORD: ${{ secrets.CODER_PASSWORD }} steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/cache@v2 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- + - uses: actions/setup-go@v2 with: - go-version: '^1.14' + go-version: '^1.16.7' + - name: integration tests run: ./ci/scripts/integration.sh diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4a0606fb..d3163c92 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,7 +1,21 @@ +name: create_github_release + on: create: tags: "v*" -name: create_github_release + +permissions: + actions: none + checks: none + contents: read + deployments: none + issues: none + packages: none + pull-requests: none + repository-projects: none + security-events: none + statuses: none + jobs: build: name: Build binaries @@ -9,49 +23,64 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-go@v2 with: - go-version: '^1.16.3' + go-version: '^1.16.7' + - name: Build run: make -j build/linux build/windows + - name: Upload linux uses: actions/upload-artifact@v2 with: name: coder-cli-linux-amd64 path: ./ci/bin/coder-cli-linux-amd64.tar.gz + - name: Upload windows uses: actions/upload-artifact@v2 with: name: coder-cli-windows path: ./ci/bin/coder-cli-windows.zip + build_darwin: name: Build darwin binary runs-on: macos-latest steps: - name: Checkout code uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Install Gon run: | brew tap mitchellh/gon brew install mitchellh/gon/gon + - name: Import Signing Certificates uses: Apple-Actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} + - uses: actions/setup-go@v2 with: - go-version: '^1.16.3' + go-version: '^1.16.7' + - name: Build Release Assets run: make build/macos env: AC_USERNAME: ${{ secrets.AC_USERNAME }} AC_PASSWORD: ${{ secrets.AC_PASSWORD }} + - name: Upload darwin uses: actions/upload-artifact@v2 with: name: coder-cli-darwin-amd64 path: ./ci/bin/coder-cli-darwin-amd64.zip + draft_release: name: Create Release runs-on: ubuntu-20.04 @@ -60,8 +89,10 @@ jobs: - build steps: - uses: actions/download-artifact@v2 + - name: content run: sh -c "ls -al" + - name: Create Release id: create_release uses: actions/create-release@v1 @@ -73,6 +104,7 @@ jobs: body: "" draft: true prerelease: false + - name: Upload Linux Release id: upload-linux-release-asset uses: actions/upload-release-asset@v1 @@ -83,6 +115,7 @@ jobs: asset_path: coder-cli-linux-amd64/coder-cli-linux-amd64.tar.gz asset_name: coder-cli-linux-amd64.tar.gz asset_content_type: application/tar+gzip + - name: Upload MacOS Release id: upload-macos-release-asset uses: actions/upload-release-asset@v1 @@ -93,6 +126,7 @@ jobs: asset_path: coder-cli-darwin-amd64/coder-cli-darwin-amd64.zip asset_name: coder-cli-darwin-amd64.zip asset_content_type: application/zip + - name: Upload Windows Release id: upload-windows-release-asset uses: actions/upload-release-asset@v1 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 79a324a9..513a3142 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,5 +1,27 @@ name: test -on: [push] + +on: + push: + branches: + - main + + pull_request: + branches: + - main + + workflow_dispatch: + +permissions: + actions: none + checks: none + contents: read + deployments: none + issues: none + packages: none + pull-requests: none + repository-projects: none + security-events: none + statuses: none jobs: fmt: From ccecc2f48f527ee9c54481ef8ad89d816ac20340 Mon Sep 17 00:00:00 2001 From: Jonathan Yu Date: Tue, 24 Aug 2021 08:41:08 -0700 Subject: [PATCH 097/128] fix: rename envs to workspaces (#421) * Fix a few stray uses of the legacy "environments" terminology and replace it with "workspaces" * Rename Workspaces as Code to Workspace Templates --- coder-sdk/workspace.go | 2 +- docs/coder_workspaces_create-from-config.md | 6 +-- docs/coder_workspaces_edit-from-config.md | 6 +-- internal/cmd/workspaces.go | 48 ++++++++++----------- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/coder-sdk/workspace.go b/coder-sdk/workspace.go index b6071b5a..cb726307 100644 --- a/coder-sdk/workspace.go +++ b/coder-sdk/workspace.go @@ -112,7 +112,7 @@ type ParseTemplateRequest struct { Local io.Reader `json:"-"` } -// TemplateVersion is a Workspaces As Code (WAC) template. +// TemplateVersion is a workspace template. // For now, let's not interpret it on the CLI level. We just need // to forward this as part of the create workspace request. type TemplateVersion struct { diff --git a/docs/coder_workspaces_create-from-config.md b/docs/coder_workspaces_create-from-config.md index 221175b7..5eed63e9 100644 --- a/docs/coder_workspaces_create-from-config.md +++ b/docs/coder_workspaces_create-from-config.md @@ -4,7 +4,7 @@ create a new workspace from a template ### Synopsis -Create a new Coder workspace using a Workspaces As Code template. +Create a new Coder workspace using a workspace template. ``` coder workspaces create-from-config [flags] @@ -14,8 +14,8 @@ coder workspaces create-from-config [flags] ``` # create a new workspace from git repository -coder envs create-from-config --name="dev-env" --repo-url https://github.com/cdr/m --ref my-branch -coder envs create-from-config --name="dev-env" -f coder.yaml +coder workspaces create-from-config --name="dev-env" --repo-url https://github.com/cdr/m --ref my-branch +coder workspaces create-from-config --name="dev-env" --filepath coder.yaml ``` ### Options diff --git a/docs/coder_workspaces_edit-from-config.md b/docs/coder_workspaces_edit-from-config.md index 085e1470..31c68cb3 100644 --- a/docs/coder_workspaces_edit-from-config.md +++ b/docs/coder_workspaces_edit-from-config.md @@ -4,7 +4,7 @@ change the template a workspace is tracking ### Synopsis -Edit an existing Coder workspace using a Workspaces As Code template. +Edit an existing Coder workspace using a workspace template. ``` coder workspaces edit-from-config [flags] @@ -14,8 +14,8 @@ coder workspaces edit-from-config [flags] ``` # edit a new workspace from git repository -coder envs edit-from-config dev-env --repo-url https://github.com/cdr/m --ref my-branch -coder envs edit-from-config dev-env -f coder.yaml +coder workspaces edit-from-config dev-env --repo-url https://github.com/cdr/m --ref my-branch +coder workspaces edit-from-config dev-env --filepath coder.yaml ``` ### Options diff --git a/internal/cmd/workspaces.go b/internal/cmd/workspaces.go index f4dd7e0f..f4d744a0 100644 --- a/internal/cmd/workspaces.go +++ b/internal/cmd/workspaces.go @@ -542,13 +542,13 @@ func selectOrg(needle string, haystack []coder.Organization) (*coder.Organizatio // If `update` is true, the update command is returned. If false, the create command. func workspaceFromConfigCmd(update bool) *cobra.Command { var ( - ref string - repo string - follow bool - filepath string - org string - providerName string - envName string + ref string + repo string + follow bool + filepath string + org string + providerName string + workspaceName string ) run := func(cmd *cobra.Command, args []string) error { @@ -556,8 +556,8 @@ func workspaceFromConfigCmd(update bool) *cobra.Command { // Update requires the env name, and the name should be the first argument. if update { - envName = args[0] - } else if envName == "" { + workspaceName = args[0] + } else if workspaceName == "" { // Create takes the name as a flag, and it must be set return clog.Error("Must provide a workspace name.", clog.BlankLine, @@ -581,17 +581,17 @@ func workspaceFromConfigCmd(update bool) *cobra.Command { } // This is the env to be updated/created - var env *coder.Workspace + var workspace *coder.Workspace // OrgID is the org where the template and env should be created. // If we are updating an env, use the orgID from the workspace. var orgID string if update { - env, err = findWorkspace(ctx, client, envName, coder.Me) + workspace, err = findWorkspace(ctx, client, workspaceName, coder.Me) if err != nil { return handleAPIError(err) } - orgID = env.OrganizationID + orgID = workspace.OrganizationID } else { var userOrg *coder.Organization // Select org in list or use default @@ -637,16 +637,16 @@ func workspaceFromConfigCmd(update bool) *cobra.Command { } if update { - err = client.EditWorkspace(ctx, env.ID, coder.UpdateWorkspaceReq{ + err = client.EditWorkspace(ctx, workspace.ID, coder.UpdateWorkspaceReq{ TemplateID: &version.TemplateID, }) } else { - env, err = client.CreateWorkspace(ctx, coder.CreateWorkspaceRequest{ + workspace, err = client.CreateWorkspace(ctx, coder.CreateWorkspaceRequest{ OrgID: orgID, TemplateID: version.TemplateID, ResourcePoolID: provider.ID, Namespace: provider.DefaultNamespace, - Name: envName, + Name: workspaceName, }) } if err != nil { @@ -655,7 +655,7 @@ func workspaceFromConfigCmd(update bool) *cobra.Command { if follow { clog.LogSuccess("creating workspace...") - if err := trailBuildLogs(ctx, client, env.ID); err != nil { + if err := trailBuildLogs(ctx, client, workspace.ID); err != nil { return err } return nil @@ -663,7 +663,7 @@ func workspaceFromConfigCmd(update bool) *cobra.Command { clog.LogSuccess("creating workspace...", clog.BlankLine, - clog.Tipf(`run "coder envs watch-build %s" to trail the build logs`, env.Name), + clog.Tipf(`run "coder workspaces watch-build %s" to see build logs`, workspace.Name), ) return nil } @@ -673,25 +673,25 @@ func workspaceFromConfigCmd(update bool) *cobra.Command { cmd = &cobra.Command{ Use: "edit-from-config", Short: "change the template a workspace is tracking", - Long: "Edit an existing Coder workspace using a Workspaces As Code template.", + Long: "Edit an existing Coder workspace using a workspace template.", Args: cobra.ExactArgs(1), Example: `# edit a new workspace from git repository -coder envs edit-from-config dev-env --repo-url https://github.com/cdr/m --ref my-branch -coder envs edit-from-config dev-env -f coder.yaml`, +coder workspaces edit-from-config dev-env --repo-url https://github.com/cdr/m --ref my-branch +coder workspaces edit-from-config dev-env --filepath coder.yaml`, RunE: run, } } else { cmd = &cobra.Command{ Use: "create-from-config", Short: "create a new workspace from a template", - Long: "Create a new Coder workspace using a Workspaces As Code template.", + Long: "Create a new Coder workspace using a workspace template.", Example: `# create a new workspace from git repository -coder envs create-from-config --name="dev-env" --repo-url https://github.com/cdr/m --ref my-branch -coder envs create-from-config --name="dev-env" -f coder.yaml`, +coder workspaces create-from-config --name="dev-env" --repo-url https://github.com/cdr/m --ref my-branch +coder workspaces create-from-config --name="dev-env" --filepath coder.yaml`, RunE: run, } cmd.Flags().StringVar(&providerName, "provider", "", "name of Workspace Provider with which to create the workspace") - cmd.Flags().StringVar(&envName, "name", "", "name of the workspace to be created") + cmd.Flags().StringVar(&workspaceName, "name", "", "name of the workspace to be created") cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the workspace should be created under.") // Ref and repo-url can only be used for create cmd.Flags().StringVarP(&ref, "ref", "", "master", "git reference to pull template from. May be a branch, tag, or commit hash.") From 6389faf976e006b0592c06e992a8f23a6ec478bc Mon Sep 17 00:00:00 2001 From: Jonathan Yu Date: Thu, 2 Sep 2021 09:04:03 -0700 Subject: [PATCH 098/128] feat: write agent output to a temporary file (#422) --- internal/cmd/agent.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go index 14bd11ab..24e61874 100644 --- a/internal/cmd/agent.go +++ b/internal/cmd/agent.go @@ -4,6 +4,7 @@ import ( "net/url" "os" "os/signal" + "path/filepath" "syscall" // We use slog here since agent runs in the background and we can benefit @@ -48,10 +49,20 @@ coder agent start coder agent start --coder-url https://my-coder.com --token xxxx-xxxx `, RunE: func(cmd *cobra.Command, args []string) error { - var ( - ctx = cmd.Context() - log = slog.Make(sloghuman.Sink(os.Stderr)).Leveled(slog.LevelDebug) - ) + ctx := cmd.Context() + sinks := []slog.Sink{ + sloghuman.Sink(os.Stderr), + } + + file, err := os.OpenFile(filepath.Join(os.TempDir(), "coder-agent.log"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err == nil && file != nil { + sinks = append(sinks, sloghuman.Sink(file)) + } + + log := slog.Make(sinks...).Leveled(slog.LevelDebug) + if err != nil { + log.Info(ctx, "failed to open agent log file", slog.Error(err)) + } if coderURL == "" { var ok bool coderURL, ok = os.LookupEnv("CODER_URL") From 1f35561128ca6e0d53bed191e348c160cb27d6c9 Mon Sep 17 00:00:00 2001 From: Jonathan Yu Date: Thu, 2 Sep 2021 12:31:15 -0700 Subject: [PATCH 099/128] feat: make log file path optional and configurable (#423) --- internal/cmd/agent.go | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go index 24e61874..9b769c13 100644 --- a/internal/cmd/agent.go +++ b/internal/cmd/agent.go @@ -4,7 +4,6 @@ import ( "net/url" "os" "os/signal" - "path/filepath" "syscall" // We use slog here since agent runs in the background and we can benefit @@ -35,34 +34,43 @@ func startCmd() *cobra.Command { var ( token string coderURL string + logFile string ) cmd := &cobra.Command{ - Use: "start --coder-url=[coder_url] --token=[token]", + Use: "start --coder-url= --token= --log-file=", Short: "starts the coder agent", Long: "starts the coder agent", Example: `# start the agent and use CODER_URL and CODER_AGENT_TOKEN env vars - coder agent start # start the agent and connect with a specified url and agent token - coder agent start --coder-url https://my-coder.com --token xxxx-xxxx + +# start the agent and write a copy of the log to /tmp/coder-agent.log +# if the file already exists, it will be truncated +coder agent start --log-file=/tmp/coder-agent.log `, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - sinks := []slog.Sink{ - sloghuman.Sink(os.Stderr), - } - file, err := os.OpenFile(filepath.Join(os.TempDir(), "coder-agent.log"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) - if err == nil && file != nil { - sinks = append(sinks, sloghuman.Sink(file)) - } + log := slog.Make(sloghuman.Sink(os.Stderr)).Leveled(slog.LevelDebug) - log := slog.Make(sinks...).Leveled(slog.LevelDebug) - if err != nil { - log.Info(ctx, "failed to open agent log file", slog.Error(err)) + // Optional log file path to write + if logFile != "" { + // Truncate the file if it already exists + file, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + // If an error occurs, log it as an error, but consider it non-fatal + log.Warn(ctx, "failed to open log file", slog.Error(err)) + } else { + // Log to both standard output and our file + log = slog.Make( + sloghuman.Sink(os.Stderr), + sloghuman.Sink(file), + ).Leveled(slog.LevelDebug) + } } + if coderURL == "" { var ok bool coderURL, ok = os.LookupEnv("CODER_URL") @@ -113,6 +121,7 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx cmd.Flags().StringVar(&token, "token", "", "coder agent token") cmd.Flags().StringVar(&coderURL, "coder-url", "", "coder access url") + cmd.Flags().StringVar(&logFile, "log-file", "", "write a copy of logs to file") return cmd } From cce78437a9a10b1cdfd34ef9f5f400bdb19603b2 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 2 Sep 2021 18:48:25 -0500 Subject: [PATCH 100/128] fix: wsnet.(*Dialer).DialContext hangs forever if ctx is cancelled (#425) --- wsnet/dial.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/wsnet/dial.go b/wsnet/dial.go index 35e13870..550735b5 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -392,19 +392,22 @@ func (d *Dialer) DialContext(ctx context.Context, network, address string) (net. ctx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() - errCh := make(chan error) + errCh := make(chan error, 1) go func() { + defer close(errCh) + var res DialChannelResponse err = json.NewDecoder(rw).Decode(&res) if err != nil { errCh <- fmt.Errorf("read dial response: %w", err) return } + d.log.Debug(ctx, "dial response", slog.F("res", res)) if res.Err == "" { - close(errCh) return } + err := errors.New(res.Err) if res.Code == CodeDialErr { err = &net.OpError{ From 229cfcf35bad6bbd574e2227f8be22ebd73d09e0 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 2 Sep 2021 19:48:23 -0500 Subject: [PATCH 101/128] chore: update wsep (#426) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e67ff7f2..2ba0211c 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.14 require ( cdr.dev/slog v1.4.1 - cdr.dev/wsep v0.0.0-20200728013649-82316a09813f + cdr.dev/wsep v0.1.0 github.com/Masterminds/semver/v3 v3.1.1 github.com/briandowns/spinner v1.16.0 github.com/cli/safeexec v1.0.0 diff --git a/go.sum b/go.sum index 0435e11b..0c5ae326 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ cdr.dev/slog v1.3.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns= cdr.dev/slog v1.4.1 h1:Q8+X63m8/WB4geelMTDO8t4CTwVh1f7+5Cxi7kS/SZg= cdr.dev/slog v1.4.1/go.mod h1:O76C6gZJxa5HK1SXMrjd48V2kJxYZKFRTcFfn/V9OhA= -cdr.dev/wsep v0.0.0-20200728013649-82316a09813f h1:WnTUINBwXE11xjp5nTVt+H2qB2/KEymos1jKMcppG9U= -cdr.dev/wsep v0.0.0-20200728013649-82316a09813f/go.mod h1:2VKClUml3gfmLez0gBxTJIjSKszpQotc2ZqPdApfK/Y= +cdr.dev/wsep v0.1.0 h1:70z0VLd9hCHuBrX/XLmhspRDL0XTAtpg/0tGqHiEIhY= +cdr.dev/wsep v0.1.0/go.mod h1:9xXBG4xn80ogx5+WHX0zlFZR2X28ECbGtakqU7ncjdM= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= From 34d97ec392210aa0ae20e38402250e411bb104a9 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 7 Sep 2021 15:03:21 -0500 Subject: [PATCH 102/128] chore: fix dial test assertions (#428) --- wsnet/dial.go | 4 ++-- wsnet/dial_test.go | 33 ++++++++++++++++++--------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/wsnet/dial.go b/wsnet/dial.go index 550735b5..c0f850b4 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -290,8 +290,8 @@ func (d *Dialer) negotiate(ctx context.Context) (err error) { return <-errCh } -// ActiveConnections returns the amount of active connections. -// DialContext opens a connection, and close will end it. +// ActiveConnections returns the amount of active connections. DialContext +// opens a connection, and close will end it. func (d *Dialer) activeConnections() int { stats, ok := d.rtc.GetStats().GetConnectionStats(d.rtc) if !ok { diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index a9b09417..4aa6f195 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -11,11 +11,12 @@ import ( "testing" "time" - "cdr.dev/slog/sloggers/slogtest" "github.com/pion/ice/v2" "github.com/pion/webrtc/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "cdr.dev/slog/sloggers/slogtest" ) func ExampleDial_basic() { @@ -260,33 +261,35 @@ func TestDial(t *testing.T) { log := slogtest.Make(t, nil) listener, err := net.Listen("tcp", "0.0.0.0:0") - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) + go func() { _, _ = listener.Accept() }() + connectAddr, listenAddr := createDumbBroker(t) _, err = Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ Log: &log, }, nil) - if err != nil { - t.Error(err) - } - conn, _ := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) + require.NoError(t, err) + + conn, err := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) + require.NoError(t, err) assert.Equal(t, 1, dialer.activeConnections()) + _ = conn.Close() assert.Equal(t, 0, dialer.activeConnections()) - _, _ = dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) - conn, _ = dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) + + _, err = dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) + require.NoError(t, err) + + conn, err = dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) + require.NoError(t, err) assert.Equal(t, 2, dialer.activeConnections()) + _ = conn.Close() assert.Equal(t, 1, dialer.activeConnections()) }) From 923b4e93e4fbe2dcf03695ba6f90a578337e0939 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 8 Sep 2021 09:58:35 +1000 Subject: [PATCH 103/128] Fork webrtc package to fix double loop (#429) --- go.mod | 8 ++++++-- go.sum | 24 +++++++++++------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 2ba0211c..b90fec09 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,10 @@ module cdr.dev/coder-cli go 1.14 +// TODO: remove the replace once this PR gets merged: +// https://github.com/pion/webrtc/pull/1946 +replace github.com/pion/webrtc/v3 => github.com/deansheather/webrtc/v3 v3.1.0-beta.6.0.20210907233552-57c66b872d12 + require ( cdr.dev/slog v1.4.1 cdr.dev/wsep v0.1.0 @@ -21,14 +25,14 @@ require ( github.com/pion/ice/v2 v2.1.12 github.com/pion/logging v0.2.2 github.com/pion/turn/v2 v2.0.5 - github.com/pion/webrtc/v3 v3.0.32 + github.com/pion/webrtc/v3 v3.1.0-beta.6.0.20210907233552-57c66b872d12 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/afero v1.6.0 github.com/spf13/cobra v1.2.1 github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 - golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d + golang.org/x/net v0.0.0-20210825183410-e898025ed96a golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 diff --git a/go.sum b/go.sum index 0c5ae326..73e8d9bb 100644 --- a/go.sum +++ b/go.sum @@ -98,6 +98,8 @@ github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deansheather/webrtc/v3 v3.1.0-beta.6.0.20210907233552-57c66b872d12 h1:NW5uk8N+M5hp5TOTcGi9a5/ZFyeG8sTVQIAKZFAWhJM= +github.com/deansheather/webrtc/v3 v3.1.0-beta.6.0.20210907233552-57c66b872d12/go.mod h1:KQH/wVKKJzBTQ6sX1bDTxsvTpQ6gEjVZSJlzYaB58aM= github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= @@ -207,7 +209,6 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -319,11 +320,10 @@ github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXm github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= github.com/pion/dtls/v2 v2.0.9 h1:7Ow+V++YSZQMYzggI0P9vLJz/hUFcffsfGMfT/Qy+u8= github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQKYQho= -github.com/pion/ice/v2 v2.1.10/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= github.com/pion/ice/v2 v2.1.12 h1:ZDBuZz+fEI7iDifZCYFVzI4p0Foy0YhdSSZ87ZtRcRE= github.com/pion/ice/v2 v2.1.12/go.mod h1:ovgYHUmwYLlRvcCLI67PnQ5YGe+upXZbGgllBDG/ktU= -github.com/pion/interceptor v0.0.13 h1:fnV+b0p/KEzwwr/9z2nsSqA9IQRMsM4nF5HjrNSWwBo= -github.com/pion/interceptor v0.0.13/go.mod h1:svsW2QoLHLoGLUr4pDoSopGBEWk8FZwlfxId/OKRKzo= +github.com/pion/interceptor v0.0.15 h1:pQFkBUL8akUHiGoFr+pM94Q/15x7sLFh0K3Nj+DCC6s= +github.com/pion/interceptor v0.0.15/go.mod h1:pg3J253eGi5bqyKzA74+ej5Y19ez2jkWANVnF+Z9Dfk= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw= @@ -332,16 +332,16 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo= github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= -github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= -github.com/pion/rtp v1.6.5 h1:o2cZf8OascA5HF/b0PAbTxRKvOWxTQxWYt7SlToxFGI= -github.com/pion/rtp v1.6.5/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/rtp v1.7.2 h1:HCDKDCixh7PVjkQTsqHAbk1lg+bx059EHxcnyl42dYs= +github.com/pion/rtp v1.7.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= github.com/pion/sctp v1.7.12 h1:GsatLufywVruXbZZT1CKg+Jr8ZTkwiPnmUC/oO9+uuY= github.com/pion/sctp v1.7.12/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8= github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= -github.com/pion/srtp/v2 v2.0.2 h1:664iGzVmaY7KYS5M0gleY0DscRo9ReDfTxQrq4UgGoU= -github.com/pion/srtp/v2 v2.0.2/go.mod h1:VEyLv4CuxrwGY8cxM+Ng3bmVy8ckz/1t6A0q/msKOw0= +github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ= +github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A= github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= @@ -352,8 +352,6 @@ github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= -github.com/pion/webrtc/v3 v3.0.32 h1:5J+zNep9am8Swh6kEMp+LaGXNvn6qQWpGkLBnVW44L4= -github.com/pion/webrtc/v3 v3.0.32/go.mod h1:wX3V5dQQUGCifhT1mYftC2kCrDQX6ZJ3B7Yad0R9JK0= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -515,9 +513,9 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210825183410-e898025ed96a h1:bRuuGXV8wwSdGTB+CtJf+FjgO1APK1CoO39T4BN/XBw= +golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= From 795a2837c04a3cb079b30398ce4044acc1d7ef17 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 7 Sep 2021 19:59:48 -0500 Subject: [PATCH 104/128] fix: use zero'd logger for Dial when no logger is passed (#430) --- wsnet/dial.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/wsnet/dial.go b/wsnet/dial.go index c0f850b4..fdddb2a8 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -8,7 +8,6 @@ import ( "io" "net" "net/url" - "os" "sync" "time" @@ -18,7 +17,6 @@ import ( "nhooyr.io/websocket" "cdr.dev/slog" - "cdr.dev/slog/sloggers/sloghuman" "cdr.dev/coder-cli/coder-sdk" ) @@ -83,8 +81,7 @@ func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, er options = &DialOptions{} } if options.Log == nil { - log := slog.Make(sloghuman.Sink(os.Stderr)).Leveled(slog.LevelInfo).Named("wsnet_dial") - options.Log = &log + options.Log = &slog.Logger{} } log := *options.Log if options.ICEServers == nil { From 276d2a0c007ef71dfc06fe30bf8dd97b1c5fc76e Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 7 Sep 2021 20:19:42 -0500 Subject: [PATCH 105/128] chore: consistent empty loggers (#431) --- wsnet/dial.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wsnet/dial.go b/wsnet/dial.go index fdddb2a8..f4410e79 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -81,7 +81,9 @@ func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, er options = &DialOptions{} } if options.Log == nil { - options.Log = &slog.Logger{} + // This logger will log nothing. + log := slog.Make() + options.Log = &log } log := *options.Log if options.ICEServers == nil { From e7bac41962a104496940a6b9a756c7df3e30b402 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 7 Sep 2021 20:19:49 -0500 Subject: [PATCH 106/128] chore: gofmt 1.17 (#432) --- internal/x/xterminal/terminal.go | 1 + internal/x/xterminal/terminal_windows.go | 1 + 2 files changed, 2 insertions(+) diff --git a/internal/x/xterminal/terminal.go b/internal/x/xterminal/terminal.go index 49ae7cba..a431fd1a 100644 --- a/internal/x/xterminal/terminal.go +++ b/internal/x/xterminal/terminal.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package xterminal diff --git a/internal/x/xterminal/terminal_windows.go b/internal/x/xterminal/terminal_windows.go index a016e5a7..bf16f099 100644 --- a/internal/x/xterminal/terminal_windows.go +++ b/internal/x/xterminal/terminal_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package xterminal From 3582a0d127898eb12cd060fa89e5b48a2bc3ed5f Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 8 Sep 2021 11:30:31 +1000 Subject: [PATCH 107/128] Upgrade to pion webrtc latest version (#433) --- go.mod | 18 +++++++++--------- go.sum | 29 +++++++++++++++++------------ 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index b90fec09..702db277 100644 --- a/go.mod +++ b/go.mod @@ -15,9 +15,9 @@ require ( github.com/fatih/color v1.12.0 github.com/google/go-cmp v0.5.6 github.com/gorilla/websocket v1.4.2 - github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 + github.com/hashicorp/yamux v0.0.0-20210826001029-26ff87cf9493 github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f - github.com/klauspost/compress v1.10.8 // indirect + github.com/klauspost/compress v1.13.5 // indirect github.com/manifoldco/promptui v0.8.0 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/pion/datachannel v1.4.21 @@ -25,18 +25,18 @@ require ( github.com/pion/ice/v2 v2.1.12 github.com/pion/logging v0.2.2 github.com/pion/turn/v2 v2.0.5 - github.com/pion/webrtc/v3 v3.1.0-beta.6.0.20210907233552-57c66b872d12 - github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 + github.com/pion/webrtc/v3 v3.1.0-beta.7 + github.com/pkg/browser v0.0.0-20210904010418-6d279e18f982 github.com/rjeczalik/notify v0.9.2 github.com/spf13/afero v1.6.0 github.com/spf13/cobra v1.2.1 github.com/stretchr/testify v1.7.0 - golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 - golang.org/x/net v0.0.0-20210825183410-e898025ed96a + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 + golang.org/x/net v0.0.0-20210907225631-ff17edfbf26d golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 - golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 - golang.org/x/time v0.0.0-20191024005414-555d28b269f0 + golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 + golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 nhooyr.io/websocket v1.8.7 ) diff --git a/go.sum b/go.sum index 73e8d9bb..908f7bcf 100644 --- a/go.sum +++ b/go.sum @@ -242,8 +242,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 h1:Y4V+SFe7d3iH+9pJCoeWIOS5/xBJIFsltS7E+KJSsJY= -github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hashicorp/yamux v0.0.0-20210826001029-26ff87cf9493 h1:brI5vBRUlAlM34VFmnLPwjnCL/FxAJp9XvOdX6Zt+XE= +github.com/hashicorp/yamux v0.0.0-20210826001029-26ff87cf9493/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -263,8 +263,8 @@ github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDS github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.10.8 h1:eLeJ3dr/Y9+XRfJT4l+8ZjmtB5RPJhucH2HeCV5+IZY= -github.com/klauspost/compress v1.10.8/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.5 h1:9O69jUPDcsT9fEm74W92rZL9FQY7rCdaXVneq+yyzl4= +github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -352,8 +352,8 @@ github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= -github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= -github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pkg/browser v0.0.0-20210904010418-6d279e18f982 h1:TdFv+3Gr3GaghJ/o80aulO4ian7GHGWMdLBXoLZH1Is= +github.com/pkg/browser v0.0.0-20210904010418-6d279e18f982/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -434,8 +434,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -514,8 +514,9 @@ golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210825183410-e898025ed96a h1:bRuuGXV8wwSdGTB+CtJf+FjgO1APK1CoO39T4BN/XBw= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210907225631-ff17edfbf26d h1:kuk8nKPQ25KCDODLCDXt99tnTVeOyOM8HGvtJ0NzAvw= +golang.org/x/net v0.0.0-20210907225631-ff17edfbf26d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -596,10 +597,13 @@ golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 h1:GkvMjFtXUmahfDtashnc1mnrCtuBVcwse5QV2lUk/tI= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -611,8 +615,9 @@ golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= From edf70b9f89f437d16f54c3a8201f14029cc56f1b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 9 Sep 2021 10:06:22 -0500 Subject: [PATCH 108/128] feat: Allow making a workspace for another user as an admin (#427) * Allow making a workspace for another user as an admin --- coder-sdk/workspace.go | 4 ++++ docs/coder_workspaces_create.md | 1 + internal/cmd/workspaces.go | 26 +++++++++++++++++++++++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/coder-sdk/workspace.go b/coder-sdk/workspace.go index cb726307..e30c5c58 100644 --- a/coder-sdk/workspace.go +++ b/coder-sdk/workspace.go @@ -89,6 +89,10 @@ type CreateWorkspaceRequest struct { Namespace string `json:"namespace"` EnableAutoStart bool `json:"autostart_enabled"` + // ForUserID is an optional param to create a workspace for another user + // other than the requester. This only works for admins and site managers. + ForUserID string `json:"for_user_id,omitempty"` + // TemplateID comes from the parse template route on cemanager. TemplateID string `json:"template_id,omitempty"` } diff --git a/docs/coder_workspaces_create.md b/docs/coder_workspaces_create.md index 56732f89..ab4f8731 100644 --- a/docs/coder_workspaces_create.md +++ b/docs/coder_workspaces_create.md @@ -33,6 +33,7 @@ coder workspaces create my-new-powerful-workspace --cpu 12 --disk 100 --memory 1 -o, --org string name of the organization the workspace should be created under. --provider string name of Workspace Provider with which to create the workspace -t, --tag string tag of the image the workspace will be based off of. (default "latest") + --user string Specify the user whose resources to target. This flag can only be used by admins and managers. Input an email or user id. (default "me") ``` ### Options inherited from parent commands diff --git a/internal/cmd/workspaces.go b/internal/cmd/workspaces.go index f4d744a0..8ac30565 100644 --- a/internal/cmd/workspaces.go +++ b/internal/cmd/workspaces.go @@ -397,6 +397,7 @@ func createWorkspaceCmd() *cobra.Command { useCVM bool providerName string enableAutostart bool + forUser string // Optional ) cmd := &cobra.Command{ @@ -448,6 +449,23 @@ coder workspaces create my-new-powerful-workspace --cpu 12 --disk 100 --memory 1 } } + var forEmail string + if forUser != "" && forUser != coder.Me { + // Making a workspace for another user, do they exist? + u, err := client.UserByEmail(ctx, forUser) + if err != nil { + // Try by ID? + u, err = client.UserByID(ctx, forUser) + if err != nil { + return xerrors.Errorf("the user %q was not found: %w", forUser, err) + } + } + forUser = u.ID + forEmail = u.Email + } else if forUser == coder.Me { + forUser = "" // coder.Me means it's not for someone else, set blank + } + // ExactArgs(1) ensures our name value can't panic on an out of bounds. createReq := &coder.CreateWorkspaceRequest{ Name: args[0], @@ -462,6 +480,7 @@ coder workspaces create my-new-powerful-workspace --cpu 12 --disk 100 --memory 1 ResourcePoolID: provider.ID, Namespace: provider.DefaultNamespace, EnableAutoStart: enableAutostart, + ForUserID: forUser, } // if any of these defaulted to their zero value we provision @@ -489,9 +508,13 @@ coder workspaces create my-new-powerful-workspace --cpu 12 --disk 100 --memory 1 return nil } + extraFlags := "" + if forEmail != coder.Me && forEmail != "" { + extraFlags = " --user " + forEmail + } clog.LogSuccess("creating workspace...", clog.BlankLine, - clog.Tipf(`run "coder workspaces watch-build %s" to trail the build logs`, workspace.Name), + clog.Tipf(`run "coder workspaces watch-build %s%s" to trail the build logs`, workspace.Name, extraFlags), ) return nil }, @@ -507,6 +530,7 @@ coder workspaces create my-new-powerful-workspace --cpu 12 --disk 100 --memory 1 cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild") cmd.Flags().BoolVar(&useCVM, "container-based-vm", false, "deploy the workspace as a Container-based VM") cmd.Flags().BoolVar(&enableAutostart, "enable-autostart", false, "automatically start this workspace at your preferred time.") + cmd.Flags().StringVar(&forUser, "user", coder.Me, "Specify the user whose resources to target. This flag can only be used by admins and managers. Input an email or user id.") _ = cmd.MarkFlagRequired("image") return cmd } From d2df9e12424160891e8d745cc78eee3441002668 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 9 Sep 2021 16:56:18 -0500 Subject: [PATCH 109/128] fix: ensure wsep processes are closed (#436) --- internal/sync/singlefile.go | 1 + internal/sync/sync.go | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/internal/sync/singlefile.go b/internal/sync/singlefile.go index adf8c290..72d7a2a0 100644 --- a/internal/sync/singlefile.go +++ b/internal/sync/singlefile.go @@ -39,6 +39,7 @@ func SingleFile(ctx context.Context, local, remoteDir string, workspace *coder.W if err != nil { return xerrors.Errorf("start sync command: %w", err) } + defer process.Close() sourceFile, err := os.Open(local) if err != nil { diff --git a/internal/sync/sync.go b/internal/sync/sync.go index dd90cf7a..bceead68 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -109,6 +109,8 @@ func (s Sync) remoteCmd(ctx context.Context, prog string, args ...string) error if err != nil { return xerrors.Errorf("exec remote process: %w", err) } + defer process.Close() + // NOTE: If the copy routine fail, it will result in `process.Wait` to unblock and report an error. go func() { _, _ = io.Copy(s.OutW, process.Stdout()) }() // Best effort. go func() { _, _ = io.Copy(s.ErrW, process.Stderr()) }() // Best effort. @@ -290,6 +292,8 @@ func (s Sync) Version() (string, error) { if err != nil { return "", err } + defer process.Close() + buf := &bytes.Buffer{} _, _ = io.Copy(buf, process.Stdout()) // Ignore error, if any, it would be handled by the process.Wait return. From 4c89550cce7ea937ed2183d917b4172970299d16 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 9 Sep 2021 18:54:18 -0500 Subject: [PATCH 110/128] fix: ensure error chan is buffered in (*Dialer).negotiate (#437) --- wsnet/dial.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/wsnet/dial.go b/wsnet/dial.go index f4410e79..53b4a186 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -193,7 +193,7 @@ type Dialer struct { func (d *Dialer) negotiate(ctx context.Context) (err error) { var ( decoder = json.NewDecoder(d.conn) - errCh = make(chan error) + errCh = make(chan error, 1) // If candidates are sent before an offer, we place them here. // We currently have no assurances to ensure this can't happen, // so it's better to buffer and process than fail. @@ -201,16 +201,12 @@ func (d *Dialer) negotiate(ctx context.Context) (err error) { ) go func() { defer close(errCh) - defer func() { - _ = d.conn.Close() - }() + defer func() { _ = d.conn.Close() }() err := waitForConnectionOpen(context.Background(), d.rtc) if err != nil { d.log.Debug(ctx, "negotiation error", slog.Error(err)) - if errors.Is(err, context.DeadlineExceeded) { - _ = d.conn.Close() - } + errCh <- fmt.Errorf("wait for connection to open: %w", err) return } @@ -331,14 +327,17 @@ func (d *Dialer) Ping(ctx context.Context) error { return err } } + d.pingMut.Lock() defer d.pingMut.Unlock() + d.log.Debug(ctx, "sending ping") _, err = d.ctrlrw.Write([]byte{'a'}) if err != nil { return fmt.Errorf("write: %w", err) } - errCh := make(chan error) + + errCh := make(chan error, 1) go func() { // There's a race in which connections can get lost-mid ping // in which case this would block forever. @@ -346,8 +345,10 @@ func (d *Dialer) Ping(ctx context.Context) error { _, err = d.ctrlrw.Read(make([]byte, 4)) errCh <- err }() - ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) - defer cancelFunc() + + ctx, cancel := context.WithTimeout(ctx, time.Second*15) + defer cancel() + select { case err := <-errCh: return err From a778c5fba3d669dd82507abaef297ff100662994 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 10 Sep 2021 10:02:14 +1000 Subject: [PATCH 111/128] Ensure rtc conn is closed on error (#438) --- wsnet/dial.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/wsnet/dial.go b/wsnet/dial.go index 53b4a186..7f00a128 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -14,6 +14,7 @@ import ( "github.com/pion/datachannel" "github.com/pion/webrtc/v3" "golang.org/x/net/proxy" + "golang.org/x/xerrors" "nhooyr.io/websocket" "cdr.dev/slog" @@ -112,6 +113,11 @@ func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, er iceServers: rtc.GetConfiguration().ICEServers, rtc: rtc.ConnectionState(), } + + closeErr := rtc.Close() + if closeErr != nil { + log.Warn(context.Background(), "close rtc connection on dial failure", slog.Error(closeErr)) + } } }() @@ -170,9 +176,12 @@ func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, er connClosers: []io.Closer{ctrl}, } - // This is on a separate line so the defer above catches it. err = dialer.negotiate(ctx) - return dialer, err + if err != nil { + return nil, xerrors.Errorf("negotiate rtc connection: %w", err) + } + + return dialer, nil } // Dialer enables arbitrary dialing to any network and address From 426b18cebde12e34f39512bb3e5060f7f57f26f5 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 10 Sep 2021 10:22:14 +1000 Subject: [PATCH 112/128] Fix dial tests (#439) --- wsnet/dial.go | 4 +++- wsnet/dial_test.go | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/wsnet/dial.go b/wsnet/dial.go index 7f00a128..8b7f4a56 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -178,7 +178,9 @@ func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, er err = dialer.negotiate(ctx) if err != nil { - return nil, xerrors.Errorf("negotiate rtc connection: %w", err) + // Return the dialer since we have tests that verify things are closed + // if negotiation fails. + return dialer, xerrors.Errorf("negotiate rtc connection: %w", err) } return dialer, nil diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 4aa6f195..d57559c3 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -62,7 +62,15 @@ func TestDial(t *testing.T) { defer cancelFunc() dialer, err := DialWebsocket(ctx, connectAddr, nil, nil) require.True(t, errors.Is(err, context.DeadlineExceeded)) + require.NotNil(t, dialer) require.Error(t, dialer.conn.Close(), "already wrote close") + + // Ensure the rtc peer connection is closed. Setting the config options + // to empty struct does nothing, but it does fail if the rtc peer conn + // is closed. + err = dialer.rtc.SetConfiguration(webrtc.Configuration{}) + require.Error(t, err) + require.ErrorIs(t, err, webrtc.ErrConnectionClosed) }) t.Run("Ping", func(t *testing.T) { From d7e206d286a6e1462d5c585cdfa8f310fc024156 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 13 Sep 2021 15:31:30 +0100 Subject: [PATCH 113/128] fix: cmd/update: do not use NewStrictVersion for user-provided version * fix: cmd/update: use `semver.NewVersion` instead of `semver.StrictNewVersion` as otherwise users must type the explicit trailing zeros e.g. coder update --version 1.21.0 instead of coder update --version 1.21. * chore: cmd/update: add unit tests for getDesiredVersion --- internal/cmd/update.go | 2 +- internal/cmd/update_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/internal/cmd/update.go b/internal/cmd/update.go index 015b807e..0eda22ad 100644 --- a/internal/cmd/update.go +++ b/internal/cmd/update.go @@ -258,7 +258,7 @@ func getDesiredVersion(httpClient getter, coderURLArg string, versionArg string) } if versionArg != "" { - desiredVersion, err = semver.StrictNewVersion(versionArg) + desiredVersion, err = semver.NewVersion(versionArg) if err != nil { return &semver.Version{}, xerrors.Errorf("parse desired version arg: %w", err) } diff --git a/internal/cmd/update_test.go b/internal/cmd/update_test.go index c751e2c4..44a3d84d 100644 --- a/internal/cmd/update_test.go +++ b/internal/cmd/update_test.go @@ -15,6 +15,7 @@ import ( "testing" "cdr.dev/slog/sloggers/slogtest/assert" + "github.com/Masterminds/semver/v3" "github.com/manifoldco/promptui" "github.com/spf13/afero" "golang.org/x/xerrors" @@ -390,6 +391,38 @@ func Test_updater_run(t *testing.T) { } } +func Test_getDesiredVersion(t *testing.T) { + t.Parallel() + + t.Run("invalid version specified by user", func(t *testing.T) { + t.Parallel() + + expected := &semver.Version{} + actual, err := getDesiredVersion(nil, "", "not a valid version") + assert.ErrorContains(t, "error should be nil", err, "Invalid Semantic Version") + assert.Equal(t, "expected should equal actual", expected, actual) + }) + + t.Run("underspecified version from user", func(t *testing.T) { + t.Parallel() + + expected, err := semver.StrictNewVersion("1.23.0") + assert.Success(t, "error should be nil", err) + actual, err := getDesiredVersion(nil, "", "1.23") + assert.Success(t, "error should be nil", err) + assert.True(t, "should handle versions without trailing zero", expected.Equal(actual)) + }) + + t.Run("empty coder URL", func(t *testing.T) { + t.Parallel() + + expected := &semver.Version{} + actual, err := getDesiredVersion(nil, "", "") + assert.ErrorContains(t, "error should be nil", err, "get coder url") + assert.True(t, "should handle versions without trailing zero", expected.Equal(actual)) + }) +} + // fakeGetter mocks HTTP requests. type fakeGetter struct { M map[string]*fakeGetterResponse From a8ef2085f4496bdde189b1dd8d4440199982e6c6 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 14 Sep 2021 03:55:42 +1000 Subject: [PATCH 114/128] Terminate connections when they reach DC limit (#443) --- wsnet/cache.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/wsnet/cache.go b/wsnet/cache.go index b16950ca..fa5e561f 100644 --- a/wsnet/cache.go +++ b/wsnet/cache.go @@ -65,6 +65,15 @@ func (d *DialerCache) evict() { // If we're no longer signaling, the connection is pending close. evict := dialer.rtc.SignalingState() == webrtc.SignalingStateClosed + + // HACK: since the pion package can't reuse data channel IDs we need + // to terminate the connection once we approach the critical number. + // We're working on adding data channel ID reuse support upstream. + stats, ok := dialer.rtc.GetStats().GetConnectionStats(dialer.rtc) + if ok && stats.DataChannelsRequested > 32500 { + evict = true + } + if dialer.activeConnections() == 0 && time.Since(d.atime[key]) >= d.ttl { evict = true } else { From 7867e8990c21021579f0410ecad2575c9bcb1733 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 14 Sep 2021 10:01:31 +0100 Subject: [PATCH 115/128] fixup! fix: cmd/update: do not use NewStrictVersion for user-provided version (#442) --- internal/cmd/update_test.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/internal/cmd/update_test.go b/internal/cmd/update_test.go index 44a3d84d..fc85b709 100644 --- a/internal/cmd/update_test.go +++ b/internal/cmd/update_test.go @@ -412,15 +412,6 @@ func Test_getDesiredVersion(t *testing.T) { assert.Success(t, "error should be nil", err) assert.True(t, "should handle versions without trailing zero", expected.Equal(actual)) }) - - t.Run("empty coder URL", func(t *testing.T) { - t.Parallel() - - expected := &semver.Version{} - actual, err := getDesiredVersion(nil, "", "") - assert.ErrorContains(t, "error should be nil", err, "get coder url") - assert.True(t, "should handle versions without trailing zero", expected.Equal(actual)) - }) } // fakeGetter mocks HTTP requests. From 582f213d183be7135e2fcf0e6f839ea320e6c2f3 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 15 Sep 2021 14:27:21 +0100 Subject: [PATCH 116/128] fix: add tip to run coder update (#445) --- internal/cmd/auth.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/cmd/auth.go b/internal/cmd/auth.go index 2c59611a..d2b8201b 100644 --- a/internal/cmd/auth.go +++ b/internal/cmd/auth.go @@ -82,5 +82,6 @@ func logVersionMismatchError(apiVersion string) { fmt.Sprintf("Coder CLI version: %s", version.Version), fmt.Sprintf("Coder API version: %s", apiVersion), clog.BlankLine, clog.Tipf("download the appropriate version here: https://github.com/cdr/coder-cli/releases"), + clog.Tipf("alternatively, run `coder update`"), ) } From 7d347713df2833729deaaed198aaa9acc8c80d0f Mon Sep 17 00:00:00 2001 From: G r e y Date: Mon, 27 Sep 2021 22:44:53 -0400 Subject: [PATCH 117/128] refactor: use constants for OSs (#448) --- internal/cmd/configssh.go | 4 ++-- internal/cmd/update.go | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go index 9740a102..a4b9f8de 100644 --- a/internal/cmd/configssh.go +++ b/internal/cmd/configssh.go @@ -160,7 +160,7 @@ func binPath() (string, error) { // Bash and OpenSSH for Windows (used by Powershell and VS Code) to function // correctly. Check if the current executable is in $PATH, and warn the user // if it isn't. - if runtime.GOOS == "windows" { + if runtime.GOOS == goosWindows { binName := filepath.Base(exePath) // We use safeexec instead of os/exec because os/exec returns paths in @@ -268,7 +268,7 @@ func makeSSHConfig(binPath, workspaceName, privateKeyFilepath string, additional fmt.Sprintf("IdentityFile=%q", privateKeyFilepath), ) - if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { + if runtime.GOOS == goosLinux || runtime.GOOS == goosDarwin { options = append(options, "ControlMaster auto", "ControlPath ~/.ssh/.connection-%r@%h:%p", diff --git a/internal/cmd/update.go b/internal/cmd/update.go index 0eda22ad..d321ab7e 100644 --- a/internal/cmd/update.go +++ b/internal/cmd/update.go @@ -36,6 +36,7 @@ import ( const ( goosWindows = "windows" goosLinux = "linux" + goosDarwin = "darwin" apiPrivateVersion = "/api/private/version" ) @@ -181,7 +182,7 @@ func (u *updater) Run(ctx context.Context, force bool, coderURLArg string, versi // TODO: validate the checksum of the downloaded file. GitHub does not currently provide this information // and we do not generate them yet. var updatedBinaryName string - if u.osF() == "windows" { + if u.osF() == goosWindows { updatedBinaryName = "coder.exe" } else { updatedBinaryName = "coder" From 34d96a1e32156619e6d014085533e88cdd482f06 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 30 Sep 2021 15:47:16 +0100 Subject: [PATCH 118/128] fix: tolerate leading v in semantic versions from coder version API (#449) * fix: tolerate leading v in semantic versions from coder version API * fix: quote reported version strings and only print Major.Minor.Patch --- internal/cmd/update.go | 16 ++++++++----- internal/cmd/update_test.go | 47 +++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/internal/cmd/update.go b/internal/cmd/update.go index d321ab7e..c9b94d80 100644 --- a/internal/cmd/update.go +++ b/internal/cmd/update.go @@ -129,14 +129,14 @@ func (u *updater) Run(ctx context.Context, force bool, coderURLArg string, versi return clog.Fatal("preflight: missing write permission on current binary") } - clog.LogInfo(fmt.Sprintf("Current version of coder-cli is %s", version.Version)) + clog.LogInfo(fmt.Sprintf("Current version of coder-cli is %q", version.Version)) desiredVersion, err := getDesiredVersion(u.httpClient, coderURLArg, versionArg) if err != nil { return clog.Fatal("failed to determine desired version of coder", clog.Causef(err.Error())) } - currentVersion, err := semver.StrictNewVersion(u.versionF()) + currentVersion, err := semver.NewVersion(u.versionF()) if err != nil { clog.LogWarn("failed to determine current version of coder-cli", clog.Causef(err.Error())) } else if currentVersion.Compare(desiredVersion) == 0 { @@ -145,7 +145,11 @@ func (u *updater) Run(ctx context.Context, force bool, coderURLArg string, versi } if !force { - label := fmt.Sprintf("Do you want to download version %s instead", desiredVersion) + label := fmt.Sprintf("Do you want to download version %d.%d.%d instead", + desiredVersion.Major(), + desiredVersion.Minor(), + desiredVersion.Patch(), + ) if _, err := u.confirmF(label); err != nil { return clog.Fatal("user cancelled operation", clog.Tipf(`use "--force" to update without confirmation`)) } @@ -240,7 +244,7 @@ func (u *updater) doUpdate(ctx context.Context, updatedCoderBinaryPath string) e if err != nil { return xerrors.Errorf("check version of updated coder binary: %w", err) } - clog.LogInfo(fmt.Sprintf("updated binary reports %s", bytes.TrimSpace(updatedVersionOutput))) + clog.LogInfo(fmt.Sprintf("updated binary reports %q", bytes.TrimSpace(updatedVersionOutput))) if err = u.fs.Rename(updatedCoderBinaryPath, u.executablePath); err != nil { return xerrors.Errorf("update coder binary in-place: %w", err) @@ -283,7 +287,7 @@ func getDesiredVersion(httpClient getter, coderURLArg string, versionArg string) return &semver.Version{}, xerrors.Errorf("query coder version: %w", err) } - clog.LogInfo(fmt.Sprintf("Coder instance at %q reports version %s", coderURL.String(), desiredVersion.String())) + clog.LogInfo(fmt.Sprintf("Coder instance at %q reports version %q", coderURL.String(), desiredVersion.String())) return desiredVersion, nil } @@ -452,7 +456,7 @@ func getAPIVersionUnauthed(client getter, baseURL url.URL) (*semver.Version, err return nil, xerrors.Errorf("parse version response: %w", err) } - version, err := semver.StrictNewVersion(ver.Version) + version, err := semver.NewVersion(ver.Version) if err != nil { return nil, xerrors.Errorf("parsing coder version: %w", err) } diff --git a/internal/cmd/update_test.go b/internal/cmd/update_test.go index fc85b709..6f0a63c1 100644 --- a/internal/cmd/update_test.go +++ b/internal/cmd/update_test.go @@ -106,6 +106,19 @@ func Test_updater_run(t *testing.T) { } run(t, "update coder - noop", func(t *testing.T, p *params) { + fakeNewVersion := "v" + fakeNewVersion + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeNewVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) + p.VersionF = func() string { return fakeNewVersion } + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assert.Success(t, "update coder - noop", err) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) + }) + + run(t, "update coder - should be noop but versions have leading v", func(t *testing.T, p *params) { + fakeNewVersion := "v" + fakeNewVersion fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeNewVersion) p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) p.VersionF = func() string { return fakeNewVersion } @@ -131,6 +144,23 @@ func Test_updater_run(t *testing.T) { assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) }) + run(t, "update coder - explicit version - leading v", func(t *testing.T, p *params) { + fakeNewVersion := "v" + fakeNewVersion + fakeOldVersion := "v" + fakeOldVersion + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeOldVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.VersionF = func() string { return fakeOldVersion } + p.ConfirmF = fakeConfirmYes + p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, fakeNewVersion) + assert.Success(t, "update coder - explicit version specified", err) + assertFileContent(t, p.Fakefs, fakeExePathLinux, strings.TrimPrefix(fakeNewVersion, "v")) // TODO: stop hard-coding this + }) + run(t, "update coder - old to new", func(t *testing.T, p *params) { fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) @@ -146,6 +176,23 @@ func Test_updater_run(t *testing.T) { assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) }) + run(t, "update coder - old to new - leading v", func(t *testing.T, p *params) { + fakeNewVersion := "v" + fakeNewVersion + fakeOldVersion := "v" + fakeOldVersion + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.VersionF = func() string { return fakeOldVersion } + p.ConfirmF = fakeConfirmYes + p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assert.Success(t, "update coder - old to new", err) + assertFileContent(t, p.Fakefs, fakeExePathLinux, strings.TrimPrefix(fakeNewVersion, "v")) // TODO: stop hard-coding this + }) + run(t, "update coder - old to new - binary renamed", func(t *testing.T, p *params) { p.ExecutablePath = "/home/user/bin/coder-cli" fakeFile(t, p.Fakefs, p.ExecutablePath, 0755, fakeOldVersion) From 66406968edca9fe3c062ad2d9a53ffdb45878a88 Mon Sep 17 00:00:00 2001 From: Armin <4032150+arminaaki@users.noreply.github.com> Date: Wed, 6 Oct 2021 15:08:32 -0400 Subject: [PATCH 119/128] allow coder login for WSL (#446) * allow coder login for WSL (#1) Co-authored-by: Armin * use constants for runtime.GOOS * update lint errors Co-authored-by: Armin --- internal/cmd/login.go | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/internal/cmd/login.go b/internal/cmd/login.go index a706259c..fe4025fd 100644 --- a/internal/cmd/login.go +++ b/internal/cmd/login.go @@ -4,7 +4,10 @@ import ( "bufio" "context" "fmt" + "io/ioutil" "net/url" + "os/exec" + "runtime" "strings" "github.com/pkg/browser" @@ -66,7 +69,8 @@ func login(cmd *cobra.Command, workspaceURL *url.URL) error { q.Add("show_token", "true") authURL.RawQuery = q.Encode() - if err := browser.OpenURL(authURL.String()); err != nil { + if err := openURL(authURL.String()); err != nil { + clog.LogWarn(err.Error()) fmt.Printf("Open the following in your browser:\n\n\t%s\n\n", authURL.String()) } else { fmt.Printf("Your browser has been opened to visit:\n\n\t%s\n\n", authURL.String()) @@ -113,3 +117,36 @@ func pingAPI(ctx context.Context, workspaceURL *url.URL, token string) error { } return nil } + +// isWSL determines if coder-cli is running within Windows Subsystem for Linux +func isWSL() (bool, error) { + if runtime.GOOS == goosDarwin || runtime.GOOS == goosWindows { + return false, nil + } + data, err := ioutil.ReadFile("/proc/version") + if err != nil { + return false, xerrors.Errorf("read /proc/version: %w", err) + } + return strings.Contains(strings.ToLower(string(data)), "microsoft"), nil +} + +// openURL opens the provided URL via user's default browser +func openURL(url string) error { + var cmd string + var args []string + + wsl, err := isWSL() + if err != nil { + return xerrors.Errorf("test running Windows Subsystem for Linux: %w", err) + } + + if wsl { + cmd = "cmd.exe" + args = []string{"/c", "start"} + url = strings.ReplaceAll(url, "&", "^&") + args = append(args, url) + return exec.Command(cmd, args...).Start() + } + + return browser.OpenURL(url) +} From 64a406e181753f49a3145539e023d57e8c8c3251 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 8 Oct 2021 18:34:01 +0100 Subject: [PATCH 120/128] feat(cli): compare hotfix version metadata (#450) * refactor: stop hard-coding tgz and zip in unit tests * feat(cli): compare cli.N build metadata to support hotfixes, add hotfix versions to asset urls * chore: update unit tests --- internal/cmd/update.go | 92 +++++++++++++++- internal/cmd/update_test.go | 211 ++++++++++++++++++++++++++++++------ 2 files changed, 265 insertions(+), 38 deletions(-) diff --git a/internal/cmd/update.go b/internal/cmd/update.go index c9b94d80..124662eb 100644 --- a/internal/cmd/update.go +++ b/internal/cmd/update.go @@ -17,7 +17,9 @@ import ( "os/exec" "path" "path/filepath" + "regexp" "runtime" + "strconv" "strings" "time" @@ -139,16 +141,26 @@ func (u *updater) Run(ctx context.Context, force bool, coderURLArg string, versi currentVersion, err := semver.NewVersion(u.versionF()) if err != nil { clog.LogWarn("failed to determine current version of coder-cli", clog.Causef(err.Error())) - } else if currentVersion.Compare(desiredVersion) == 0 { + } else if compareVersions(currentVersion, desiredVersion) == 0 { clog.LogInfo("Up to date!") return nil } if !force { - label := fmt.Sprintf("Do you want to download version %d.%d.%d instead", + prerelease := "" + if desiredVersion.Prerelease() != "" { + prerelease = "-" + desiredVersion.Prerelease() + } + hotfix := "" + if hotfixVersion(desiredVersion) != "" { + hotfix = hotfixVersion(desiredVersion) + } + label := fmt.Sprintf("Do you want to download version %d.%d.%d%s%s instead", desiredVersion.Major(), desiredVersion.Minor(), desiredVersion.Patch(), + prerelease, + hotfix, ) if _, err := u.confirmF(label); err != nil { return clog.Fatal("user cancelled operation", clog.Tipf(`use "--force" to update without confirmation`)) @@ -218,7 +230,7 @@ func (u *updater) Run(ctx context.Context, force bool, coderURLArg string, versi return clog.Fatal("failed to update coder binary", clog.Causef(err.Error())) } - clog.LogSuccess("Updated coder CLI to version " + desiredVersion.String()) + clog.LogSuccess("Updated coder CLI") return nil } @@ -308,6 +320,7 @@ func queryGithubAssetURL(httpClient getter, version *semver.Version, ostype stri fmt.Fprint(&b, "-") fmt.Fprint(&b, version.Prerelease()) } + fmt.Fprintf(&b, "%s", hotfixVersion(version)) // this will be empty if no hotfix urlString := fmt.Sprintf("https://api.github.com/repos/cdr/coder-cli/releases/tags/v%s", b.String()) clog.LogInfo("query github releases", fmt.Sprintf("url: %q", urlString)) @@ -493,3 +506,76 @@ func HasFilePathPrefix(s, prefix string) bool { func defaultExec(ctx context.Context, cmd string, args ...string) ([]byte, error) { return exec.CommandContext(ctx, cmd, args...).CombinedOutput() } + +// hotfixExpr matches the build metadata used for identifying CLI hotfixes. +var hotfixExpr = regexp.MustCompile(`(?i)^.*?cli\.(\d+).*?$`) + +// hotfixVersion returns the hotfix build metadata tag if it is present in v +// and an empty string otherwise. +func hotfixVersion(v *semver.Version) string { + match := hotfixExpr.FindStringSubmatch(v.Metadata()) + if len(match) < 2 { + return "" + } + + return fmt.Sprintf("+cli.%s", match[1]) +} + +// compareVersions performs a NON-SEMVER-COMPLIANT comparison of two versions. +// If the two versions differ as per SemVer, then that result is returned. +// Otherwise, the build metadata of the two versions are compared based on +// the `cli.N` hotfix metadata. +// +// Examples: +// compareVersions(semver.MustParse("v1.0.0"), semver.MustParse("v1.0.0")) +// 0 +// compareVersions(semver.MustParse("v1.0.0"), semver.MustParse("v1.0.1")) +// 1 +// compareVersions(semver.MustParse("v1.0.1"), semver.MustParse("v1.0.0")) +// -1 +// compareVersions(semver.MustParse("v1.0.0+cli.0"), semver.MustParse("v1.0.0")) +// 1 +// compareVersions(semver.MustParse("v1.0.0+cli.0"), semver.MustParse("v1.0.0+cli.0")) +// 0 +// compareVersions(semver.MustParse("v1.0.0"), semver.MustParse("v1.0.0+cli.0")) +// -1 +// compareVersions(semver.MustParse("v1.0.0+cli.1"), semver.MustParse("v1.0.0+cli.0")) +// 1 +// compareVersions(semver.MustParse("v1.0.0+cli.0"), semver.MustParse("v1.0.0+cli.1")) +// -1 +// +func compareVersions(a, b *semver.Version) int { + semverComparison := a.Compare(b) + if semverComparison != 0 { + return semverComparison + } + + matchA := hotfixExpr.FindStringSubmatch(a.Metadata()) + matchB := hotfixExpr.FindStringSubmatch(b.Metadata()) + + hotfixA := -1 + hotfixB := -1 + + // extract hotfix versions from the metadata of a and b + if len(matchA) > 1 { + if n, err := strconv.Atoi(matchA[1]); err == nil { + hotfixA = n + } + } + if len(matchB) > 1 { + if n, err := strconv.Atoi(matchB[1]); err == nil { + hotfixB = n + } + } + + // compare hotfix versions + if hotfixA < hotfixB { + return -1 + } + if hotfixA > hotfixB { + return 1 + } + // both versions are the same if their semver and hotfix + // metadata are the same. + return 0 +} diff --git a/internal/cmd/update_test.go b/internal/cmd/update_test.go index 6f0a63c1..00855a72 100644 --- a/internal/cmd/update_test.go +++ b/internal/cmd/update_test.go @@ -1,10 +1,13 @@ package cmd import ( + "archive/tar" + "archive/zip" "bytes" + "compress/gzip" "context" - "encoding/base64" "fmt" + "io" "io/fs" "io/ioutil" "net/http" @@ -29,9 +32,11 @@ const ( fakeCoderURL = "https://my.cdr.dev" fakeNewVersion = "1.23.4-rc.5+678-gabcdef-12345678" fakeOldVersion = "1.22.4-rc.5+678-gabcdef-12345678" + fakeHotfixVersion = "1.23.4-rc.5+678-gabcdef-12345678.cli.2" filenameLinux = "coder-cli-linux-amd64.tar.gz" filenameWindows = "coder-cli-windows.zip" fakeGithubReleaseURL = "https://api.github.com/repos/cdr/coder-cli/releases/tags/v1.23.4-rc.5" + fakeGithubHotfixURL = fakeGithubReleaseURL + "+cli.2" ) var ( @@ -39,9 +44,15 @@ var ( fakeError = xerrors.New("fake error for testing") fakeNewVersionJSON = fmt.Sprintf(`{"version":%q}`, fakeNewVersion) fakeOldVersionJSON = fmt.Sprintf(`{"version":%q}`, fakeOldVersion) + fakeHotfixVersionJSON = fmt.Sprintf(`{"version":%q}`, fakeHotfixVersion) + fakeNewVersionTgz = mustValidTgz("coder", []byte(fakeNewVersion), 0751) + fakeHotfixVersionTgz = mustValidTgz("coder", []byte(fakeHotfixVersion), 0751) + fakeNewVersionZip = mustValidZip("coder.exe", []byte(fakeNewVersion)) + fakeHotfixVersionZip = mustValidZip("coder.exe", []byte(fakeHotfixVersion)) fakeAssetURLLinux = "https://github.com/cdr/coder-cli/releases/download/v1.23.4-rc.5/" + filenameLinux fakeAssetURLWindows = "https://github.com/cdr/coder-cli/releases/download/v1.23.4-rc.5/" + filenameWindows - fakeGithubReleaseJSON = fmt.Sprintf(`{"assets":[{"name":%q,"browser_download_url":%q},{"name":%q,"browser_download_url":%q}]}`, filenameLinux, fakeAssetURLLinux, filenameWindows, fakeAssetURLWindows) + fakeHotfixURLLinux = "https://github.com/cdr/coder-cli/releases/download/v1.23.4-rc.5+cli.2/" + filenameLinux + fakeHotfixURLWindows = "https://github.com/cdr/coder-cli/releases/download/v1.23.4-rc.5+cli.2/" + filenameWindows ) func Test_updater_run(t *testing.T) { @@ -132,8 +143,8 @@ func Test_updater_run(t *testing.T) { run(t, "update coder - explicit version specified", func(t *testing.T, p *params) { fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeOldVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) p.VersionF = func() string { return fakeOldVersion } p.ConfirmF = fakeConfirmYes p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} @@ -149,8 +160,8 @@ func Test_updater_run(t *testing.T) { fakeOldVersion := "v" + fakeOldVersion fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeOldVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) p.VersionF = func() string { return fakeOldVersion } p.ConfirmF = fakeConfirmYes p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} @@ -164,8 +175,8 @@ func Test_updater_run(t *testing.T) { run(t, "update coder - old to new", func(t *testing.T, p *params) { fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) p.VersionF = func() string { return fakeOldVersion } p.ConfirmF = fakeConfirmYes p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} @@ -181,8 +192,8 @@ func Test_updater_run(t *testing.T) { fakeOldVersion := "v" + fakeOldVersion fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) p.VersionF = func() string { return fakeOldVersion } p.ConfirmF = fakeConfirmYes p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} @@ -193,12 +204,44 @@ func Test_updater_run(t *testing.T) { assertFileContent(t, p.Fakefs, fakeExePathLinux, strings.TrimPrefix(fakeNewVersion, "v")) // TODO: stop hard-coding this }) + run(t, "update coder - new to hotfix", func(t *testing.T, p *params) { + fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeNewVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeHotfixVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubHotfixURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeHotfixURLLinux), 200, variadicS(), nil) + p.HTTPClient.M[fakeHotfixURLLinux] = newFakeGetterResponse(fakeHotfixVersionTgz, 200, variadicS(), nil) + p.VersionF = func() string { return fakeNewVersion } + p.ConfirmF = fakeConfirmYes + p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assert.Success(t, "update coder - new to hotfix", err) + assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeHotfixVersion) + }) + + run(t, "update coder - new to hotfix - windows", func(t *testing.T, p *params) { + p.OsF = func() string { return goosWindows } + p.ExecutablePath = fakeExePathWindows + fakeFile(t, p.Fakefs, fakeExePathWindows, 0755, fakeNewVersion) + p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeHotfixVersionJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubHotfixURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameWindows, fakeHotfixURLWindows), 200, variadicS(), nil) + p.HTTPClient.M[fakeHotfixURLWindows] = newFakeGetterResponse(fakeHotfixVersionZip, 200, variadicS(), nil) + p.VersionF = func() string { return fakeNewVersion } + p.ConfirmF = fakeConfirmYes + p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} + u := fromParams(p) + assertFileContent(t, p.Fakefs, fakeExePathWindows, fakeNewVersion) + err := u.Run(p.Ctx, false, fakeCoderURL, "") + assert.Success(t, "update coder - new to hotfix", err) + assertFileContent(t, p.Fakefs, fakeExePathWindows, fakeHotfixVersion) + }) + run(t, "update coder - old to new - binary renamed", func(t *testing.T, p *params) { p.ExecutablePath = "/home/user/bin/coder-cli" fakeFile(t, p.Fakefs, p.ExecutablePath, 0755, fakeOldVersion) p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) p.VersionF = func() string { return fakeOldVersion } p.ConfirmF = fakeConfirmYes p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} @@ -214,8 +257,8 @@ func Test_updater_run(t *testing.T) { p.ExecutablePath = fakeExePathWindows fakeFile(t, p.Fakefs, fakeExePathWindows, 0755, fakeOldVersion) p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLWindows] = newFakeGetterResponse(fakeValidZipBytes, 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameWindows, fakeAssetURLWindows), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLWindows] = newFakeGetterResponse(fakeNewVersionZip, 200, variadicS(), nil) p.VersionF = func() string { return fakeOldVersion } p.ConfirmF = fakeConfirmYes p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} @@ -229,8 +272,8 @@ func Test_updater_run(t *testing.T) { run(t, "update coder - old to new forced", func(t *testing.T, p *params) { fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) p.VersionF = func() string { return fakeOldVersion } p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} u := fromParams(p) @@ -314,7 +357,7 @@ func Test_updater_run(t *testing.T) { run(t, "update coder - failed to fetch URL", func(t *testing.T, p *params) { fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse([]byte{}, 0, variadicS(), fakeError) p.VersionF = func() string { return fakeOldVersion } p.ConfirmF = fakeConfirmYes @@ -328,7 +371,7 @@ func Test_updater_run(t *testing.T) { run(t, "update coder - release URL 404", func(t *testing.T, p *params) { fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse([]byte{}, 404, variadicS(), nil) p.VersionF = func() string { return fakeOldVersion } p.ConfirmF = fakeConfirmYes @@ -342,7 +385,7 @@ func Test_updater_run(t *testing.T) { run(t, "update coder - invalid tgz archive", func(t *testing.T, p *params) { fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse([]byte{}, 200, variadicS(), nil) p.VersionF = func() string { return fakeOldVersion } p.ConfirmF = fakeConfirmYes @@ -358,7 +401,7 @@ func Test_updater_run(t *testing.T) { p.ExecutablePath = fakeExePathWindows fakeFile(t, p.Fakefs, fakeExePathWindows, 0755, fakeOldVersion) p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameWindows, fakeAssetURLWindows), 200, variadicS(), nil) p.HTTPClient.M[fakeAssetURLWindows] = newFakeGetterResponse([]byte{}, 200, variadicS(), nil) p.VersionF = func() string { return fakeOldVersion } p.ConfirmF = fakeConfirmYes @@ -374,8 +417,8 @@ func Test_updater_run(t *testing.T) { p.Fakefs = afero.NewReadOnlyFs(rwfs) fakeFile(t, rwfs, fakeExePathLinux, 0755, fakeOldVersion) p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) p.VersionF = func() string { return fakeOldVersion } p.ConfirmF = fakeConfirmYes u := fromParams(p) @@ -388,8 +431,8 @@ func Test_updater_run(t *testing.T) { run(t, "update coder - cannot exec new binary", func(t *testing.T, p *params) { fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte(fakeGithubReleaseJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeValidTgzBytes, 200, variadicS(), nil) + p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) + p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) p.VersionF = func() string { return fakeOldVersion } p.ConfirmF = fakeConfirmYes p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{nil, fakeError} @@ -461,6 +504,34 @@ func Test_getDesiredVersion(t *testing.T) { }) } +func Test_compareVersions(t *testing.T) { + t.Parallel() + + testCases := []struct { + Name string + V1 string + V2 string + Expected int + }{ + {"old vs old", fakeOldVersion, fakeOldVersion, 0}, + {"old vs new", fakeOldVersion, fakeNewVersion, -1}, + {"old vs hotfix", fakeOldVersion, fakeHotfixVersion, -1}, + {"new vs old", fakeNewVersion, fakeOldVersion, 1}, + {"new vs new", fakeNewVersion, fakeNewVersion, 0}, + {"new vs hotfix", fakeNewVersion, fakeHotfixVersion, -1}, + {"hotfix vs old", fakeHotfixVersion, fakeOldVersion, 1}, + {"hotfix vs new", fakeHotfixVersion, fakeNewVersion, 1}, + {"hotfix vs hotfix", fakeHotfixVersion, fakeHotfixVersion, 0}, + } + for _, testCase := range testCases { + testCase := testCase + v1 := semver.MustParse(testCase.V1) + v2 := semver.MustParse(testCase.V2) + actual := compareVersions(v1, v2) + assert.Equal(t, testCase.Name+": expected comparison differs", testCase.Expected, actual) + } +} + // fakeGetter mocks HTTP requests. type fakeGetter struct { M map[string]*fakeGetterResponse @@ -574,18 +645,75 @@ func assertCLIError(t *testing.T, name string, err error, expectedHeader, expect } } -// this is a valid tgz archive containing a single file named 'coder' with permissions 0751 -// containing the string "1.23.4-rc.5+678-gabcdef-12345678". -var fakeValidTgzBytes, _ = base64.StdEncoding.DecodeString(`H4sIAAAAAAAAA+3QsQ4CIRCEYR6F3oC7wIqvc3KnpQnq+3tGCwsTK3LN/zWTTDWZuG/XeeluJFlV -s1dqNfnOtyJOi4qllHOuTlSTqPMydNXH43afuvfu3w3jb9qExpRjCb1F2x3qMVymU5uXc9CUi63F -1vsAAAAAAAAAAAAAAAAAAL89AYuL424AKAAA`) +// mustValidTgz creates a valid tgz file and panics if any error is encountered. +// only for use in unit tests. +func mustValidTgz(filename string, data []byte, perms os.FileMode) []byte { + must := func(err error, msg string) { + if err != nil { + panic(xerrors.Errorf("%s: %w", msg, err)) + } + } + fs := afero.NewMemMapFs() + // populate memfs with file + f, err := fs.Create(filename) + must(err, "create file") + _, err = f.Write(data) + must(err, "write data") + err = f.Close() + must(err, "close file") + err = fs.Chmod(filename, perms) + must(err, "set perms") + + // create archive from fs + + f, err = fs.Open(filename) + must(err, "open file") + fsinfo, err := f.Stat() + must(err, "stat file") + header, err := tar.FileInfoHeader(fsinfo, fsinfo.Name()) + must(err, "create tar header") + header.Name = filename + + var buf bytes.Buffer + gw := gzip.NewWriter(&buf) + tw := tar.NewWriter(gw) + + err = tw.WriteHeader(header) + must(err, "write header") + _, err = io.Copy(tw, f) + must(err, "write file") + err = f.Close() + must(err, "close file") + err = tw.Close() + must(err, "close tar writer") + err = gw.Close() + must(err, "close gzip writer") + + return buf.Bytes() +} + +// mustValidZip creates a valid zip file and panics if any error is encountered. +// only for use in unit tests. +func mustValidZip(filename string, data []byte) []byte { + must := func(err error, msg string) { + if err != nil { + panic(xerrors.Errorf("%s: %w", msg, err)) + } + } + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + w, err := zw.Create(filename) + must(err, "create zip archive") + _, err = io.Copy(w, bytes.NewReader(data)) + must(err, "write file") + err = zw.Close() + must(err, "close gzip writer") + + return buf.Bytes() +} -// this is a valid zip archive containing a single file named 'coder.exe' with permissions 0751 -// containing the string "1.23.4-rc.5+678-gabcdef-12345678". -var fakeValidZipBytes, _ = base64.StdEncoding.DecodeString(`UEsDBAoAAAAAAAtfDVNCHNDCIAAAACAAAAAJABwAY29kZXIuZXhlVVQJAAPmXRZh/10WYXV4CwAB -BOgDAAAE6AMAADEuMjMuNC1yYy41KzY3OC1nYWJjZGVmLTEyMzQ1Njc4UEsBAh4DCgAAAAAAC18N -U0Ic0MIgAAAAIAAAAAkAGAAAAAAAAQAAAO2BAAAAAGNvZGVyLmV4ZVVUBQAD5l0WYXV4CwABBOgD -AAAE6AMAAFBLBQYAAAAAAQABAE8AAABjAAAAAAA=`) +var _ = mustValidTgz("testing", []byte("testing"), 0777) +var _ = mustValidZip("testing", []byte("testing")) type fakeExecer struct { M map[string]fakeExecerResult @@ -614,3 +742,16 @@ type fakeExecerResult struct { Output []byte Err error } + +func fakeGithubReleaseJSON(filename, assetURL string) []byte { + jsonStr := fmt.Sprintf(` + {"assets": + [ + { + "name": %q, + "browser_download_url": %q + } + ] + }`, filename, assetURL) + return []byte(jsonStr) +} From 89ecc34810cd49c56cce6b1750cafbc3a3bf14ee Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 21 Oct 2021 08:53:33 +1000 Subject: [PATCH 121/128] Add note to README about close sourced (#451) --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9757c3ec..f1c40c55 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,22 @@ [![GitHub Release](https://img.shields.io/github/v/release/cdr/coder-cli?color=6b9ded&include_prerelease=false)](https://github.com/cdr/coder-cli/releases) [![Documentation](https://godoc.org/cdr.dev/coder-cli?status.svg)](https://pkg.go.dev/cdr.dev/coder-cli/coder-sdk) -![build](https://github.com/cdr/coder-cli/workflows/build/badge.svg) -[![Go Report Card](https://goreportcard.com/badge/cdr.dev/coder-cli)](https://goreportcard.com/report/cdr.dev/coder-cli) `coder` is a command line utility for Coder. To report bugs and request features, please [open an issue](https://github.com/cdr/coder-cli/issues/new). +## Code + +As of v1.24.0, the Coder CLI is closed source. The code in this repo will remain +as it was when closed on 20 October 2021. We will continue to use issues and +releases for the time being, but this may change. + +We recommend using the SDK included in this repo until we publish the new Go SDK +that's currently in progress. + +We will not accept any further pull requests. + ## Installation ### Homebrew (Mac, Linux) From 0ceb7e6495dfae2ad6c9f1cc0b4b90f5b8d982eb Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 16 Dec 2021 08:54:32 +1000 Subject: [PATCH 122/128] Increment version to 1.26.0 --- version | 1 + 1 file changed, 1 insertion(+) create mode 100644 version diff --git a/version b/version new file mode 100644 index 00000000..5ff8c4f5 --- /dev/null +++ b/version @@ -0,0 +1 @@ +1.26.0 From abc1a862d3af9771f38ddea5155df5700a5fcfbe Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 6 Jan 2022 01:51:38 +0000 Subject: [PATCH 123/128] Update version --- version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version b/version index 5ff8c4f5..dd43a143 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.26.0 +1.26.1 From 9a9a1f360fdc1775eaa5a43a4e88a1be466bbb1b Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Tue, 19 Jul 2022 18:42:49 +0200 Subject: [PATCH 124/128] chore: add note this is the CLI for Coder Classic (#485) --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f1c40c55..548e9903 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ [![GitHub Release](https://img.shields.io/github/v/release/cdr/coder-cli?color=6b9ded&include_prerelease=false)](https://github.com/cdr/coder-cli/releases) [![Documentation](https://godoc.org/cdr.dev/coder-cli?status.svg)](https://pkg.go.dev/cdr.dev/coder-cli/coder-sdk) -`coder` is a command line utility for Coder. - -To report bugs and request features, please [open an issue](https://github.com/cdr/coder-cli/issues/new). +> **Note**: This is the command line utility for [Coder Classic](https://coder.com/docs/coder). +> If you are using [Coder OSS](https://coder.com/docs/coder-oss/latest), use [these instructions](https://coder.com/docs/coder-oss/latest/install) +> to install the CLI. ## Code @@ -18,6 +18,10 @@ that's currently in progress. We will not accept any further pull requests. +## Bugs & feature requests + +To report bugs and request features, please [open an issue](https://github.com/cdr/coder-cli/issues/new). + ## Installation ### Homebrew (Mac, Linux) From 3a6edf62f472f666f6344a35f1190cd26dcb1611 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 20 Jul 2022 14:18:56 -0500 Subject: [PATCH 125/128] feat: add username to 'coder ws ls' (#486) * feat: add username to 'coder ws ls' * make lint * make lint --- internal/cmd/login.go | 4 +- internal/coderutil/workspace.go | 81 ++++++++++++++++++++++++++------- wsnet/listen.go | 2 + 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/internal/cmd/login.go b/internal/cmd/login.go index fe4025fd..69d9bfd2 100644 --- a/internal/cmd/login.go +++ b/internal/cmd/login.go @@ -118,7 +118,7 @@ func pingAPI(ctx context.Context, workspaceURL *url.URL, token string) error { return nil } -// isWSL determines if coder-cli is running within Windows Subsystem for Linux +// isWSL determines if coder-cli is running within Windows Subsystem for Linux. func isWSL() (bool, error) { if runtime.GOOS == goosDarwin || runtime.GOOS == goosWindows { return false, nil @@ -130,7 +130,7 @@ func isWSL() (bool, error) { return strings.Contains(strings.ToLower(string(data)), "microsoft"), nil } -// openURL opens the provided URL via user's default browser +// openURL opens the provided URL via user's default browser. func openURL(url string) error { var cmd string var args []string diff --git a/internal/coderutil/workspace.go b/internal/coderutil/workspace.go index b81ad964..8dba71cd 100644 --- a/internal/coderutil/workspace.go +++ b/internal/coderutil/workspace.go @@ -79,14 +79,15 @@ func DefaultWorkspaceProvider(ctx context.Context, c coder.Client) (*coder.Kuber // WorkspaceTable defines an Workspace-like structure with associated entities composed in a human // readable form. type WorkspaceTable struct { - Name string `table:"Name"` - Image string `table:"Image"` - CPU float32 `table:"vCPU"` - MemoryGB float32 `table:"MemoryGB"` - DiskGB int `table:"DiskGB"` - Status string `table:"Status"` - Provider string `table:"Provider"` - CVM bool `table:"CVM"` + Name string `table:"Name" json:"name"` + Image string `table:"Image" json:"image"` + CPU float32 `table:"vCPU" json:"cpu"` + MemoryGB float32 `table:"MemoryGB" json:"memory_gb"` + DiskGB int `table:"DiskGB" json:"disk_gb"` + Status string `table:"Status" json:"status"` + Provider string `table:"Provider" json:"provider"` + CVM bool `table:"CVM" json:"cvm"` + Username string `table:"Username" json:"username"` } // WorkspacesHumanTable performs the composition of each Workspace with its associated ProviderName and ImageRepo. @@ -96,6 +97,11 @@ func WorkspacesHumanTable(ctx context.Context, client coder.Client, workspaces [ return nil, err } + userMap, err := MakeUserMap(ctx, client, workspaces) + if err != nil { + return nil, err + } + pooledWorkspaces := make([]WorkspaceTable, 0, len(workspaces)) providers, err := client.WorkspaceProviders(ctx) if err != nil { @@ -105,25 +111,66 @@ func WorkspacesHumanTable(ctx context.Context, client coder.Client, workspaces [ for _, p := range providers.Kubernetes { providerMap[p.ID] = p } - for _, e := range workspaces { - workspaceProvider, ok := providerMap[e.ResourcePoolID] + for _, ws := range workspaces { + workspaceProvider, ok := providerMap[ws.ResourcePoolID] if !ok { return nil, xerrors.Errorf("fetch workspace workspace provider: %w", coder.ErrNotFound) } pooledWorkspaces = append(pooledWorkspaces, WorkspaceTable{ - Name: e.Name, - Image: fmt.Sprintf("%s:%s", imageMap[e.ImageID].Repository, e.ImageTag), - CPU: e.CPUCores, - MemoryGB: e.MemoryGB, - DiskGB: e.DiskGB, - Status: string(e.LatestStat.ContainerStatus), + Name: ws.Name, + Image: fmt.Sprintf("%s:%s", imageMap[ws.ImageID].Repository, ws.ImageTag), + CPU: ws.CPUCores, + MemoryGB: ws.MemoryGB, + DiskGB: ws.DiskGB, + Status: string(ws.LatestStat.ContainerStatus), Provider: workspaceProvider.Name, - CVM: e.UseContainerVM, + CVM: ws.UseContainerVM, + Username: userMap[ws.UserID].Username, }) } return pooledWorkspaces, nil } +func MakeUserMap(ctx context.Context, client coder.Client, workspaces []coder.Workspace) (map[string]*coder.User, error) { + var ( + mu sync.Mutex + egroup = clog.LoggedErrGroup() + ) + + userMap := map[string]*coder.User{} + + // Iterate over all the workspaces to get a list of unique User IDs. + for _, ws := range workspaces { + userMap[ws.UserID] = nil + } + + fetchIds := make([]string, 0, len(userMap)) + for id := range userMap { + fetchIds = append(fetchIds, id) + } + + for _, id := range fetchIds { + id := id + egroup.Go(func() error { + user, err := client.UserByID(ctx, id) + if err != nil { + return xerrors.Errorf("get user by id: %w", err) + } + mu.Lock() + defer mu.Unlock() + + userMap[id] = user + return nil + }) + } + + if err := egroup.Wait(); err != nil { + return nil, xerrors.Errorf("fetch all workspace users: %w", err) + } + + return userMap, nil +} + // MakeImageMap fetches all image entities specified in the slice of workspaces, then places them into an ID map. func MakeImageMap(ctx context.Context, client coder.Client, workspaces []coder.Workspace) (map[string]*coder.Image, error) { var ( diff --git a/wsnet/listen.go b/wsnet/listen.go index 78002899..6bee01c1 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -116,6 +116,8 @@ func (l *listener) dial(ctx context.Context) (<-chan error, error) { _ = l.ws.Close(websocket.StatusNormalClosure, "new connection inbound") } + // websocket lib documents that the response does not need to be closed. + // nolint conn, resp, err := websocket.Dial(ctx, l.broker, nil) if err != nil { if resp != nil { From 097fb8f9a2ee2787f0e41d7262de0ac3395da759 Mon Sep 17 00:00:00 2001 From: Geoffrey Huntley Date: Thu, 5 Jan 2023 09:33:51 +1030 Subject: [PATCH 126/128] docs(readme): reduce a11y so coder v2 users don't stumble upon this (#501) --- README.md | 41 ++--------------------------------------- 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 548e9903..5660b5b5 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,7 @@ -# Coder CLI +# Coder v1 CLI [![GitHub Release](https://img.shields.io/github/v/release/cdr/coder-cli?color=6b9ded&include_prerelease=false)](https://github.com/cdr/coder-cli/releases) [![Documentation](https://godoc.org/cdr.dev/coder-cli?status.svg)](https://pkg.go.dev/cdr.dev/coder-cli/coder-sdk) -> **Note**: This is the command line utility for [Coder Classic](https://coder.com/docs/coder). -> If you are using [Coder OSS](https://coder.com/docs/coder-oss/latest), use [these instructions](https://coder.com/docs/coder-oss/latest/install) -> to install the CLI. +> **Note**: This is the command line utility for [Coder Classic](https://coder.com/docs/coder). If you are using [Coder v2 / Coder OSS](https://coder.com/docs/coder-oss/latest), use [these instructions](https://coder.com/docs/coder-oss/latest/install) to install the CLI. -## Code - -As of v1.24.0, the Coder CLI is closed source. The code in this repo will remain -as it was when closed on 20 October 2021. We will continue to use issues and -releases for the time being, but this may change. - -We recommend using the SDK included in this repo until we publish the new Go SDK -that's currently in progress. - -We will not accept any further pull requests. - -## Bugs & feature requests - -To report bugs and request features, please [open an issue](https://github.com/cdr/coder-cli/issues/new). - -## Installation - -### Homebrew (Mac, Linux) - -```sh -brew install cdr/coder/coder-cli -``` - -### Download (Windows, Linux, Mac) - -Download the latest [release](https://github.com/cdr/coder-cli/releases): - -1. Click a release and download the tar file for your operating system (ex: coder-cli-linux-amd64.tar.gz) -2. Extract the `coder` binary. - -## Usage - -View the usage documentation [here](./docs/coder.md). - -You can find additional Coder usage documentation on [coder.com/docs/cli](https://coder.com/docs/cli). From 3fa10bb21becef660dcc48529382aa9a1b983269 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 30 Jan 2023 20:52:45 -0600 Subject: [PATCH 127/128] chore: add more v1 warnings (#505) --- .github/workflows/release.yaml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d3163c92..622cb0d2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -101,7 +101,7 @@ jobs: with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} - body: "" + body: "> **Note**: This is the command line utility for [Coder v1](https://coder.com/docs/coder). If you are using [Coder v2 / Coder OSS](https://coder.com/docs/coder-oss/latest), use [these instructions](https://coder.com/docs/coder-oss/latest/install) to install the CLI." draft: true prerelease: false diff --git a/README.md b/README.md index 5660b5b5..8c36eca7 100644 --- a/README.md +++ b/README.md @@ -3,5 +3,5 @@ [![GitHub Release](https://img.shields.io/github/v/release/cdr/coder-cli?color=6b9ded&include_prerelease=false)](https://github.com/cdr/coder-cli/releases) [![Documentation](https://godoc.org/cdr.dev/coder-cli?status.svg)](https://pkg.go.dev/cdr.dev/coder-cli/coder-sdk) -> **Note**: This is the command line utility for [Coder Classic](https://coder.com/docs/coder). If you are using [Coder v2 / Coder OSS](https://coder.com/docs/coder-oss/latest), use [these instructions](https://coder.com/docs/coder-oss/latest/install) to install the CLI. +> **Note**: This is the command line utility for [Coder v1](https://coder.com/docs/coder). If you are using [Coder v2 / Coder OSS](https://coder.com/docs/coder-oss/latest), use [these instructions](https://coder.com/docs/coder-oss/latest/install) to install the CLI. From 85b12ffa60fa0d07a9be2946888c342b7ae79a04 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Thu, 16 Feb 2023 14:29:52 +0400 Subject: [PATCH 128/128] Remove outdated sources; binary only on this repo (#508) Signed-off-by: Spike Curtis --- .github/dependabot.yml | 30 - .github/workflows/build.yaml | 81 -- .github/workflows/integration.yaml | 53 - .github/workflows/release.yaml | 139 --- .github/workflows/test.yaml | 96 -- .gitignore | 9 - .golangci.yml | 120 --- Makefile | 51 - README.md | 8 +- ci/README.md | 34 - ci/gon.json | 10 - ci/image/Dockerfile | 8 - ci/integration/Dockerfile | 3 - ci/integration/integration_test.go | 115 -- ci/integration/login_test.go | 75 -- ci/integration/setup_test.go | 68 -- ci/integration/ssh_test.go | 33 - ci/integration/statictokens_test.go | 50 - ci/scripts/build.sh | 54 - ci/scripts/files_changed.sh | 12 - ci/scripts/integration.sh | 11 - cmd/coder/main.go | 56 - coder-sdk/README.md | 11 - coder-sdk/activity.go | 27 - coder-sdk/activity_test.go | 51 - coder-sdk/agent.go | 22 - coder-sdk/client.go | 126 --- coder-sdk/client_test.go | 155 --- coder-sdk/config.go | 226 ---- coder-sdk/devurl.go | 63 -- coder-sdk/doc.go | 2 - coder-sdk/error.go | 92 -- coder-sdk/image.go | 101 -- coder-sdk/interface.go | 260 ----- coder-sdk/login.go | 69 -- coder-sdk/org.go | 101 -- coder-sdk/registries.go | 60 -- coder-sdk/request.go | 127 --- coder-sdk/satellite.go | 51 - coder-sdk/tags.go | 72 -- coder-sdk/tokens.go | 67 -- coder-sdk/users.go | 165 --- coder-sdk/users_test.go | 109 -- coder-sdk/util.go | 36 - coder-sdk/version.go | 23 - coder-sdk/webrtc.go | 23 - coder-sdk/workspace.go | 515 --------- coder-sdk/workspace_providers.go | 140 --- coder-sdk/ws.go | 35 - docs/coder.md | 27 - docs/coder_completion.md | 70 -- docs/coder_config-ssh.md | 31 - docs/coder_images.md | 26 - docs/coder_images_ls.md | 31 - docs/coder_login.md | 24 - docs/coder_logout.md | 24 - docs/coder_satellites.md | 27 - docs/coder_satellites_create.md | 36 - docs/coder_satellites_ls.md | 35 - docs/coder_satellites_rm.md | 35 - docs/coder_ssh.md | 31 - docs/coder_sync.md | 25 - docs/coder_tokens.md | 29 - docs/coder_tokens_create.md | 24 - docs/coder_tokens_ls.md | 25 - docs/coder_tokens_regen.md | 24 - docs/coder_tokens_rm.md | 24 - docs/coder_update.md | 31 - docs/coder_urls.md | 23 - docs/coder_urls_create.md | 33 - docs/coder_urls_ls.md | 25 - docs/coder_urls_rm.md | 24 - docs/coder_users.md | 21 - docs/coder_users_ls.md | 32 - docs/coder_workspaces.md | 35 - docs/coder_workspaces_create-from-config.md | 43 - docs/coder_workspaces_create.md | 48 - docs/coder_workspaces_edit-from-config.md | 38 - docs/coder_workspaces_edit.md | 46 - docs/coder_workspaces_ls.md | 32 - docs/coder_workspaces_ping.md | 36 - docs/coder_workspaces_policy-template.md | 32 - docs/coder_workspaces_rebuild.md | 34 - docs/coder_workspaces_rm.md | 26 - docs/coder_workspaces_stop.md | 44 - docs/coder_workspaces_watch-build.md | 31 - go.mod | 42 - go.sum | 819 -------------- internal/activity/pusher.go | 47 - internal/activity/writer.go | 24 - internal/cmd/agent.go | 127 --- internal/cmd/auth.go | 87 -- internal/cmd/ceapi.go | 243 ----- internal/cmd/cli_test.go | 169 --- internal/cmd/cmd.go | 122 --- internal/cmd/configssh.go | 314 ------ internal/cmd/devurls_test.go | 11 - internal/cmd/errors.go | 71 -- internal/cmd/images.go | 88 -- internal/cmd/images_test.go | 27 - internal/cmd/login.go | 152 --- internal/cmd/logout.go | 32 - internal/cmd/providers.go | 319 ------ internal/cmd/providers_test.go | 11 - internal/cmd/rebuild.go | 186 ---- internal/cmd/resourcemanager.go | 433 -------- internal/cmd/resourcemanager_test.go | 184 ---- internal/cmd/resourcemanager_test.golden | 32 - internal/cmd/satellites.go | 222 ---- internal/cmd/ssh.go | 121 --- internal/cmd/sync.go | 121 --- internal/cmd/tags.go | 173 --- internal/cmd/tags_test.go | 31 - internal/cmd/tokens.go | 136 --- internal/cmd/tunnel.go | 297 ----- internal/cmd/update.go | 581 ---------- internal/cmd/update_test.go | 757 ------------- internal/cmd/urls.go | 270 ----- internal/cmd/users.go | 61 -- internal/cmd/users_test.go | 32 - internal/cmd/workspaces.go | 1074 ------------------- internal/cmd/workspaces_test.go | 235 ---- internal/coderutil/doc.go | 2 - internal/coderutil/provider.go | 21 - internal/coderutil/workspace.go | 209 ---- internal/config/dir.go | 52 - internal/config/doc.go | 3 - internal/config/file.go | 26 - internal/sync/eventcache.go | 61 -- internal/sync/singlefile.go | 61 -- internal/sync/sync.go | 397 ------- internal/sync/title.go | 17 - internal/version/version.go | 20 - internal/version/version_test.go | 33 - internal/x/xcobra/cobra.go | 25 - internal/x/xsync/doc.go | 2 - internal/x/xsync/syncwriter.go | 24 - internal/x/xterminal/doc.go | 13 - internal/x/xterminal/terminal.go | 25 - internal/x/xterminal/terminal_windows.go | 49 - pkg/clog/clog.go | 135 --- pkg/clog/clog_test.go | 85 -- pkg/clog/doc.go | 7 - pkg/clog/errgroup.go | 58 - pkg/clog/errgroup_test.go | 43 - pkg/proto/doc.go | 2 - pkg/tablewriter/doc.go | 2 - pkg/tablewriter/table_output.golden | 3 - pkg/tablewriter/tablewriter.go | 96 -- pkg/tablewriter/tablewriter_test.go | 67 -- pkg/tcli/doc.go | 4 - pkg/tcli/tcli.go | 352 ------ pkg/tcli/tcli_test.go | 70 -- version | 1 - wsnet/cache.go | 170 --- wsnet/cache_test.go | 71 -- wsnet/conn.go | 200 ---- wsnet/dial.go | 454 -------- wsnet/dial_test.go | 425 -------- wsnet/doc.go | 3 - wsnet/error.go | 39 - wsnet/listen.go | 479 --------- wsnet/listen_test.go | 51 - wsnet/proto.go | 137 --- wsnet/proto_test.go | 235 ---- wsnet/rtc.go | 290 ----- wsnet/rtc_test.go | 83 -- wsnet/wsnet_test.go | 195 ---- 168 files changed, 7 insertions(+), 17468 deletions(-) delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/build.yaml delete mode 100644 .github/workflows/integration.yaml delete mode 100644 .github/workflows/release.yaml delete mode 100644 .github/workflows/test.yaml delete mode 100644 .gitignore delete mode 100644 .golangci.yml delete mode 100644 Makefile delete mode 100644 ci/README.md delete mode 100644 ci/gon.json delete mode 100644 ci/image/Dockerfile delete mode 100644 ci/integration/Dockerfile delete mode 100644 ci/integration/integration_test.go delete mode 100644 ci/integration/login_test.go delete mode 100644 ci/integration/setup_test.go delete mode 100644 ci/integration/ssh_test.go delete mode 100644 ci/integration/statictokens_test.go delete mode 100755 ci/scripts/build.sh delete mode 100755 ci/scripts/files_changed.sh delete mode 100755 ci/scripts/integration.sh delete mode 100644 cmd/coder/main.go delete mode 100644 coder-sdk/README.md delete mode 100644 coder-sdk/activity.go delete mode 100644 coder-sdk/activity_test.go delete mode 100644 coder-sdk/agent.go delete mode 100644 coder-sdk/client.go delete mode 100644 coder-sdk/client_test.go delete mode 100644 coder-sdk/config.go delete mode 100644 coder-sdk/devurl.go delete mode 100644 coder-sdk/doc.go delete mode 100644 coder-sdk/error.go delete mode 100644 coder-sdk/image.go delete mode 100644 coder-sdk/interface.go delete mode 100644 coder-sdk/login.go delete mode 100644 coder-sdk/org.go delete mode 100644 coder-sdk/registries.go delete mode 100644 coder-sdk/request.go delete mode 100644 coder-sdk/satellite.go delete mode 100644 coder-sdk/tags.go delete mode 100644 coder-sdk/tokens.go delete mode 100644 coder-sdk/users.go delete mode 100644 coder-sdk/users_test.go delete mode 100644 coder-sdk/util.go delete mode 100644 coder-sdk/version.go delete mode 100644 coder-sdk/webrtc.go delete mode 100644 coder-sdk/workspace.go delete mode 100644 coder-sdk/workspace_providers.go delete mode 100644 coder-sdk/ws.go delete mode 100644 docs/coder.md delete mode 100644 docs/coder_completion.md delete mode 100644 docs/coder_config-ssh.md delete mode 100644 docs/coder_images.md delete mode 100644 docs/coder_images_ls.md delete mode 100644 docs/coder_login.md delete mode 100644 docs/coder_logout.md delete mode 100644 docs/coder_satellites.md delete mode 100644 docs/coder_satellites_create.md delete mode 100644 docs/coder_satellites_ls.md delete mode 100644 docs/coder_satellites_rm.md delete mode 100644 docs/coder_ssh.md delete mode 100644 docs/coder_sync.md delete mode 100644 docs/coder_tokens.md delete mode 100644 docs/coder_tokens_create.md delete mode 100644 docs/coder_tokens_ls.md delete mode 100644 docs/coder_tokens_regen.md delete mode 100644 docs/coder_tokens_rm.md delete mode 100644 docs/coder_update.md delete mode 100644 docs/coder_urls.md delete mode 100644 docs/coder_urls_create.md delete mode 100644 docs/coder_urls_ls.md delete mode 100644 docs/coder_urls_rm.md delete mode 100644 docs/coder_users.md delete mode 100644 docs/coder_users_ls.md delete mode 100644 docs/coder_workspaces.md delete mode 100644 docs/coder_workspaces_create-from-config.md delete mode 100644 docs/coder_workspaces_create.md delete mode 100644 docs/coder_workspaces_edit-from-config.md delete mode 100644 docs/coder_workspaces_edit.md delete mode 100644 docs/coder_workspaces_ls.md delete mode 100644 docs/coder_workspaces_ping.md delete mode 100644 docs/coder_workspaces_policy-template.md delete mode 100644 docs/coder_workspaces_rebuild.md delete mode 100644 docs/coder_workspaces_rm.md delete mode 100644 docs/coder_workspaces_stop.md delete mode 100644 docs/coder_workspaces_watch-build.md delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 internal/activity/pusher.go delete mode 100644 internal/activity/writer.go delete mode 100644 internal/cmd/agent.go delete mode 100644 internal/cmd/auth.go delete mode 100644 internal/cmd/ceapi.go delete mode 100644 internal/cmd/cli_test.go delete mode 100644 internal/cmd/cmd.go delete mode 100644 internal/cmd/configssh.go delete mode 100644 internal/cmd/devurls_test.go delete mode 100644 internal/cmd/errors.go delete mode 100644 internal/cmd/images.go delete mode 100644 internal/cmd/images_test.go delete mode 100644 internal/cmd/login.go delete mode 100644 internal/cmd/logout.go delete mode 100644 internal/cmd/providers.go delete mode 100644 internal/cmd/providers_test.go delete mode 100644 internal/cmd/rebuild.go delete mode 100644 internal/cmd/resourcemanager.go delete mode 100644 internal/cmd/resourcemanager_test.go delete mode 100755 internal/cmd/resourcemanager_test.golden delete mode 100644 internal/cmd/satellites.go delete mode 100644 internal/cmd/ssh.go delete mode 100644 internal/cmd/sync.go delete mode 100644 internal/cmd/tags.go delete mode 100644 internal/cmd/tags_test.go delete mode 100644 internal/cmd/tokens.go delete mode 100644 internal/cmd/tunnel.go delete mode 100644 internal/cmd/update.go delete mode 100644 internal/cmd/update_test.go delete mode 100644 internal/cmd/urls.go delete mode 100644 internal/cmd/users.go delete mode 100644 internal/cmd/users_test.go delete mode 100644 internal/cmd/workspaces.go delete mode 100644 internal/cmd/workspaces_test.go delete mode 100644 internal/coderutil/doc.go delete mode 100644 internal/coderutil/provider.go delete mode 100644 internal/coderutil/workspace.go delete mode 100644 internal/config/dir.go delete mode 100644 internal/config/doc.go delete mode 100644 internal/config/file.go delete mode 100644 internal/sync/eventcache.go delete mode 100644 internal/sync/singlefile.go delete mode 100644 internal/sync/sync.go delete mode 100644 internal/sync/title.go delete mode 100644 internal/version/version.go delete mode 100644 internal/version/version_test.go delete mode 100644 internal/x/xcobra/cobra.go delete mode 100644 internal/x/xsync/doc.go delete mode 100644 internal/x/xsync/syncwriter.go delete mode 100644 internal/x/xterminal/doc.go delete mode 100644 internal/x/xterminal/terminal.go delete mode 100644 internal/x/xterminal/terminal_windows.go delete mode 100644 pkg/clog/clog.go delete mode 100644 pkg/clog/clog_test.go delete mode 100644 pkg/clog/doc.go delete mode 100644 pkg/clog/errgroup.go delete mode 100644 pkg/clog/errgroup_test.go delete mode 100644 pkg/proto/doc.go delete mode 100644 pkg/tablewriter/doc.go delete mode 100755 pkg/tablewriter/table_output.golden delete mode 100644 pkg/tablewriter/tablewriter.go delete mode 100644 pkg/tablewriter/tablewriter_test.go delete mode 100644 pkg/tcli/doc.go delete mode 100644 pkg/tcli/tcli.go delete mode 100644 pkg/tcli/tcli_test.go delete mode 100644 version delete mode 100644 wsnet/cache.go delete mode 100644 wsnet/cache_test.go delete mode 100644 wsnet/conn.go delete mode 100644 wsnet/dial.go delete mode 100644 wsnet/dial_test.go delete mode 100644 wsnet/doc.go delete mode 100644 wsnet/error.go delete mode 100644 wsnet/listen.go delete mode 100644 wsnet/listen_test.go delete mode 100644 wsnet/proto.go delete mode 100644 wsnet/proto_test.go delete mode 100644 wsnet/rtc.go delete mode 100644 wsnet/rtc_test.go delete mode 100644 wsnet/wsnet_test.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index f2976dc1..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,30 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - time: "06:00" - timezone: "America/Chicago" - commit-message: - prefix: "chore" - ignore: - # These actions deliver the latest versions by updating the major - # release tag, so ignore minor and patch versions - - dependency-name: "actions/*" - update-types: - - "version-update:semver-minor" - - "version-update:semver-patch" - - dependency-name: "Apple-Actions/import-codesign-certs" - update-types: - - "version-update:semver-minor" - - "version-update:semver-patch" - - - package-ecosystem: "gomod" - directory: "/" - schedule: - interval: "daily" - time: "06:00" - timezone: "America/Chicago" - commit-message: - prefix: "chore" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index 33c5b836..00000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,81 +0,0 @@ -name: build - -on: - push: - branches: - - main - - pull_request: - branches: - - main - - workflow_dispatch: - -permissions: - actions: none - checks: none - contents: read - deployments: none - issues: none - packages: none - pull-requests: none - repository-projects: none - security-events: none - statuses: none - -jobs: - build: - runs-on: ubuntu-20.04 - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - uses: actions/setup-go@v2 - with: - go-version: '^1.16.7' - - - name: Build - run: make -j build/linux build/windows - - - name: Upload - uses: actions/upload-artifact@v2 - with: - name: coder-cli - path: ./ci/bin/coder-cli-* - - build_darwin: - runs-on: macos-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - uses: actions/setup-go@v2 - with: - go-version: '^1.16.7' - - - name: Install Gon - run: | - brew tap mitchellh/gon - brew install mitchellh/gon/gon - - - name: Import Signing Certificates - uses: Apple-Actions/import-codesign-certs@v1 - with: - p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} - p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} - - - name: Build - run: make build/macos - env: - AC_USERNAME: ${{ secrets.AC_USERNAME }} - AC_PASSWORD: ${{ secrets.AC_PASSWORD }} - - - name: Upload - uses: actions/upload-artifact@v2 - with: - name: coder-cli - path: ./ci/bin/coder-cli-* diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml deleted file mode 100644 index bb1a39d4..00000000 --- a/.github/workflows/integration.yaml +++ /dev/null @@ -1,53 +0,0 @@ -name: integration - -on: - push: - branches: - - main - - pull_request: - branches: - - main - - schedule: - - cron: '*/180 * * * *' - - workflow_dispatch: - -permissions: - actions: none - checks: none - contents: read - deployments: none - issues: none - packages: none - pull-requests: none - repository-projects: none - security-events: none - statuses: none - -jobs: - integration: - runs-on: ubuntu-latest - env: - CODER_URL: ${{ secrets.CODER_URL }} - CODER_EMAIL: ${{ secrets.CODER_EMAIL }} - CODER_PASSWORD: ${{ secrets.CODER_PASSWORD }} - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - uses: actions/cache@v2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - - uses: actions/setup-go@v2 - with: - go-version: '^1.16.7' - - - name: integration tests - run: ./ci/scripts/integration.sh diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 622cb0d2..00000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,139 +0,0 @@ -name: create_github_release - -on: - create: - tags: "v*" - -permissions: - actions: none - checks: none - contents: read - deployments: none - issues: none - packages: none - pull-requests: none - repository-projects: none - security-events: none - statuses: none - -jobs: - build: - name: Build binaries - runs-on: ubuntu-20.04 - steps: - - name: Checkout code - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - uses: actions/setup-go@v2 - with: - go-version: '^1.16.7' - - - name: Build - run: make -j build/linux build/windows - - - name: Upload linux - uses: actions/upload-artifact@v2 - with: - name: coder-cli-linux-amd64 - path: ./ci/bin/coder-cli-linux-amd64.tar.gz - - - name: Upload windows - uses: actions/upload-artifact@v2 - with: - name: coder-cli-windows - path: ./ci/bin/coder-cli-windows.zip - - build_darwin: - name: Build darwin binary - runs-on: macos-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Install Gon - run: | - brew tap mitchellh/gon - brew install mitchellh/gon/gon - - - name: Import Signing Certificates - uses: Apple-Actions/import-codesign-certs@v1 - with: - p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} - p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} - - - uses: actions/setup-go@v2 - with: - go-version: '^1.16.7' - - - name: Build Release Assets - run: make build/macos - env: - AC_USERNAME: ${{ secrets.AC_USERNAME }} - AC_PASSWORD: ${{ secrets.AC_PASSWORD }} - - - name: Upload darwin - uses: actions/upload-artifact@v2 - with: - name: coder-cli-darwin-amd64 - path: ./ci/bin/coder-cli-darwin-amd64.zip - - draft_release: - name: Create Release - runs-on: ubuntu-20.04 - needs: - - build_darwin - - build - steps: - - uses: actions/download-artifact@v2 - - - name: content - run: sh -c "ls -al" - - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: ${{ github.ref }} - body: "> **Note**: This is the command line utility for [Coder v1](https://coder.com/docs/coder). If you are using [Coder v2 / Coder OSS](https://coder.com/docs/coder-oss/latest), use [these instructions](https://coder.com/docs/coder-oss/latest/install) to install the CLI." - draft: true - prerelease: false - - - name: Upload Linux Release - id: upload-linux-release-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: coder-cli-linux-amd64/coder-cli-linux-amd64.tar.gz - asset_name: coder-cli-linux-amd64.tar.gz - asset_content_type: application/tar+gzip - - - name: Upload MacOS Release - id: upload-macos-release-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: coder-cli-darwin-amd64/coder-cli-darwin-amd64.zip - asset_name: coder-cli-darwin-amd64.zip - asset_content_type: application/zip - - - name: Upload Windows Release - id: upload-windows-release-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: coder-cli-windows/coder-cli-windows.zip - asset_name: coder-cli-windows.zip - asset_content_type: application/zip diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index 513a3142..00000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,96 +0,0 @@ -name: test - -on: - push: - branches: - - main - - pull_request: - branches: - - main - - workflow_dispatch: - -permissions: - actions: none - checks: none - contents: read - deployments: none - issues: none - packages: none - pull-requests: none - repository-projects: none - security-events: none - statuses: none - -jobs: - fmt: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - - uses: actions/cache@v2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - - name: fmt - uses: ./ci/image - with: - args: make -j fmt - - - run: ./ci/scripts/files_changed.sh - - lint: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - - name: golangci-lint - uses: golangci/golangci-lint-action@v2.5.2 - with: - # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.39 - - test: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - - uses: actions/cache@v2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - - name: test - uses: ./ci/image - env: - COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CODER_URL: ${{ secrets.CODER_URL }} - CODER_EMAIL: ${{ secrets.CODER_EMAIL }} - CODER_PASSWORD: ${{ secrets.CODER_PASSWORD }} - with: - args: make -j test/coverage - - gendocs: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - - uses: actions/cache@v2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - - name: generate-docs - uses: ./ci/image - with: - args: make -j gendocs - - - run: ./ci/scripts/files_changed.sh diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 11433c18..00000000 --- a/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -./coder -.idea -ci/bin -cmd/coder/coder -ci/integration/bin -ci/integration/env.sh -coder-sdk/env.sh -.vscode -vendor \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index df85d74b..00000000 --- a/.golangci.yml +++ /dev/null @@ -1,120 +0,0 @@ -# See https://golangci-lint.run/usage/configuration/ -linters-settings: - gocognit: - # tunnel.go has a 150 line function. Someone should fix it and - # decrement this back down to a rational number. - min-complexity: 52 - goconst: - min-len: 4 - min-occurrences: 3 - nestif: - min-complexity: 10 - revive: - # see https://github.com/mgechev/revive#available-rules for details. - ignore-generated-header: true - severity: warning - rules: - - name: atomic - # - name: bare-return - - name: blank-imports - - name: bool-literal-in-expr - - name: call-to-gc - - name: confusing-naming - - name: confusing-results - - name: constant-logical-expr - - name: context-as-argument - - name: context-keys-type - # - name: deep-exit - - name: defer - - name: dot-imports - - name: duplicated-imports - # - name: early-return - # - name: empty-block - - name: empty-lines - - name: error-naming - - name: error-return - - name: error-strings - - name: errorf - - name: exported - # - name: flag-parameter - - name: get-return - - name: identical-branches - # - name: if-return - # - name: import-shadowing - - name: increment-decrement - - name: indent-error-flow - - name: modifies-parameter - - name: modifies-value-receiver - - name: package-comments - - name: range - - name: range-val-address - - name: range-val-in-closure - - name: receiver-naming - - name: redefines-builtin-id - - name: string-of-int - - name: struct-tag - - name: superfluous-else - - name: time-naming - - name: unconditional-recursion - - name: unexported-naming - - name: unexported-return - # - name: unhandled-error - - name: unnecessary-stmt - - name: unreachable-code - # - name: unused-parameter - # - name: unused-receiver - # - name: var-declaration - - name: var-naming - - name: waitgroup-by-value - -issues: - fix: true - max-issues-per-linter: 0 - max-same-issues: 0 - -run: - timeout: 5m - -linters: - disable-all: true - enable: - - bodyclose - - deadcode - - dogsled - - errcheck - # - errorlint - - exportloopref - # - forcetypeassert - - gocognit - - goconst - - gocritic - - gocyclo - - godot - - gofmt - - goimports - - golint - - gomodguard - - goprintffuncname - # - gosec - - gosimple - - govet - - ineffassign - - makezero - - megacheck - - misspell - - nestif - - noctx - - nolintlint - - revive - - rowserrcheck - - sqlclosecheck - - staticcheck - - structcheck - - stylecheck - - typecheck - - unconvert - - unparam - - unused - - varcheck - - wastedassign - - whitespace diff --git a/Makefile b/Makefile deleted file mode 100644 index aa80df36..00000000 --- a/Makefile +++ /dev/null @@ -1,51 +0,0 @@ -# Makefile for Coder CLI - -.PHONY: clean build build/macos build/windows build/linux fmt lint gendocs test/go dev - -PROJECT_ROOT := $(shell git rev-parse --show-toplevel) -MAKE_ROOT := $(shell pwd) - -clean: - rm -rf ./ci/bin - -build: build/macos build/windows build/linux - -build/macos: - # requires darwin - CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 ./ci/scripts/build.sh -build/windows: - CGO_ENABLED=0 GOOS=windows ./ci/scripts/build.sh -build/linux: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 ./ci/scripts/build.sh - -fmt: - go mod tidy - gofmt -w -s . - goimports -w "-local=$$(go list -m)" . - -lint: - golangci-lint run -c .golangci.yml - -gendocs: - rm -rf ./docs - mkdir ./docs - go run ./cmd/coder gen-docs ./docs - -test/go: - go test $$(go list ./... | grep -v pkg/tcli | grep -v ci/integration) - -test/coverage: - go test \ - -race \ - -covermode atomic \ - -coverprofile coverage \ - $$(go list ./... | grep -v pkg/tcli | grep -v ci/integration) - - goveralls -coverprofile=coverage -service=github - -dev: build/linux - @echo "removing project root binary if exists" - -rm ./coder - @echo "untarring..." - @tar -xzf ./ci/bin/coder-cli-linux-amd64.tar.gz - @echo "new dev binary ready" diff --git a/README.md b/README.md index 8c36eca7..4fbeb053 100644 --- a/README.md +++ b/README.md @@ -3,5 +3,11 @@ [![GitHub Release](https://img.shields.io/github/v/release/cdr/coder-cli?color=6b9ded&include_prerelease=false)](https://github.com/cdr/coder-cli/releases) [![Documentation](https://godoc.org/cdr.dev/coder-cli?status.svg)](https://pkg.go.dev/cdr.dev/coder-cli/coder-sdk) -> **Note**: This is the command line utility for [Coder v1](https://coder.com/docs/coder). If you are using [Coder v2 / Coder OSS](https://coder.com/docs/coder-oss/latest), use [these instructions](https://coder.com/docs/coder-oss/latest/install) to install the CLI. +This is the command line utility for [Coder v1](https://coder.com/docs/coder). If you are using +[Coder v2 / Coder OSS](https://coder.com/docs/coder-oss/latest), use +[these instructions](https://coder.com/docs/coder-oss/latest/install) to install the CLI. +The Coder v1 CLI is now closed-source. You may download binary releases from this repo. + +[Coder v2](https://coder.com/docs/coder-oss/latest) is open-source and the recommended +version for new Coder users. \ No newline at end of file diff --git a/ci/README.md b/ci/README.md deleted file mode 100644 index 1daee639..00000000 --- a/ci/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# ci - -## checks - -- `steps/build.sh` builds release assets with the appropriate tag injected. Required to pass for merging. -- `steps/fmt.sh` formats all Go source files. -- `steps/gendocs.sh` generates CLI documentation into `/docs` from the command specifications. -- `steps/lint.sh` lints all Go source files based on the rules set fourth in `/.golangci.yml`. - - -## integration tests - -### `tcli` - -Package `tcli` provides a framework for writing end-to-end CLI tests. -Each test group can have its own container for executing commands in a consistent -and isolated filesystem. - -### running - -Assign the following environment variables to run the integration tests -against an existing Enterprise deployment instance. - -```bash -export CODER_URL=... -export CODER_EMAIL=... -export CODER_PASSWORD=... -``` - -Then, simply run the test command from the project root - -```sh -./ci/steps/integration.sh -``` diff --git a/ci/gon.json b/ci/gon.json deleted file mode 100644 index 0762638f..00000000 --- a/ci/gon.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "source": ["./coder"], - "bundle_id": "com.coder.cli", - "sign": { - "application_identity": "3C4F31D15F9D57461A8D7D0BD970D23CE1F7C2BE" - }, - "zip": { - "output_path": "coder.zip" - } -} \ No newline at end of file diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile deleted file mode 100644 index b77bf8aa..00000000 --- a/ci/image/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM golang:1.16.5 - -ENV GOFLAGS="-mod=readonly" -ENV CI=true - -RUN go get golang.org/x/tools/cmd/goimports -RUN go get github.com/mattn/goveralls -RUN apt update && apt install grep diff --git a/ci/integration/Dockerfile b/ci/integration/Dockerfile deleted file mode 100644 index 70dcc2c0..00000000 --- a/ci/integration/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM ubuntu:20.04 - -RUN apt-get update && apt-get install -y jq curl build-essential diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go deleted file mode 100644 index 8654965a..00000000 --- a/ci/integration/integration_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package integration - -import ( - "context" - "math/rand" - "testing" - "time" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/pkg/tcli" -) - -func run(t *testing.T, container string, execute func(t *testing.T, ctx context.Context, runner *tcli.ContainerRunner)) { - t.Run(container, func(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute) - defer cancel() - - c, err := tcli.NewContainerRunner(ctx, &tcli.ContainerConfig{ - Image: "coder-cli-integration:latest", - Name: container, - BindMounts: map[string]string{ - binpath: "/bin/coder", - }, - }) - assert.Success(t, "new run container", err) - defer c.Close() - - execute(t, ctx, c) - }) -} - -func TestCoderCLI(t *testing.T) { - t.Parallel() - run(t, "test-coder-cli", func(t *testing.T, ctx context.Context, c *tcli.ContainerRunner) { - c.Run(ctx, "which coder").Assert(t, - tcli.Success(), - tcli.StderrEmpty(), - ) - - c.Run(ctx, "coder --version").Assert(t, - tcli.StderrEmpty(), - tcli.Success(), - tcli.StdoutMatches("linux"), - ) - - c.Run(ctx, "coder --help").Assert(t, - tcli.Success(), - tcli.StdoutMatches("Available Commands"), - ) - - headlessLogin(ctx, t, c) - - c.Run(ctx, "coder workspaces").Assert(t, - tcli.Success(), - ) - - c.Run(ctx, "coder workspaces ls").Assert(t, - tcli.Success(), - ) - - c.Run(ctx, "coder workspaces ls -o json").Assert(t, - tcli.Success(), - ) - - c.Run(ctx, "coder tokens").Assert(t, - tcli.Success(), - ) - - c.Run(ctx, "coder tokens ls").Assert(t, - tcli.Success(), - ) - - c.Run(ctx, "coder tokens ls -o json").Assert(t, - tcli.Success(), - ) - - c.Run(ctx, "coder urls").Assert(t, - tcli.Success(), - ) - - c.Run(ctx, "coder sync").Assert(t, - tcli.Error(), - ) - - c.Run(ctx, "coder sh").Assert(t, - tcli.Error(), - ) - - c.Run(ctx, "coder logout").Assert(t, - tcli.Success(), - ) - - c.Run(ctx, "coder workspaces ls").Assert(t, - tcli.Error(), - ) - - c.Run(ctx, "coder tokens ls").Assert(t, - tcli.Error(), - ) - }) -} - -var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) - -func randString(length int) string { - const charset = "abcdefghijklmnopqrstuvwxyz" - b := make([]byte, length) - for i := range b { - b[i] = charset[seededRand.Intn(len(charset))] - } - return string(b) -} diff --git a/ci/integration/login_test.go b/ci/integration/login_test.go deleted file mode 100644 index e0334f00..00000000 --- a/ci/integration/login_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package integration - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "net/url" - "os" - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" -) - -type credentials struct { - url, token string -} - -func login(ctx context.Context, t *testing.T) credentials { - var ( - email = requireEnv(t, "CODER_EMAIL") - password = requireEnv(t, "CODER_PASSWORD") - rawURL = requireEnv(t, "CODER_URL") - ) - sessionToken := getSessionToken(ctx, t, email, password, rawURL) - - return credentials{ - url: rawURL, - token: sessionToken, - } -} - -func requireEnv(t *testing.T, key string) string { - value := os.Getenv(key) - assert.True(t, fmt.Sprintf("%q is nonempty", key), value != "") - return value -} - -type loginBuiltInAuthReq struct { - Email string `json:"email"` - Password string `json:"password"` -} - -type loginBuiltInAuthResp struct { - SessionToken string `json:"session_token"` -} - -func getSessionToken(ctx context.Context, t *testing.T, email, password, rawURL string) string { - reqbody := loginBuiltInAuthReq{ - Email: email, - Password: password, - } - body, err := json.Marshal(reqbody) - assert.Success(t, "marshal login req body", err) - - u, err := url.Parse(rawURL) - assert.Success(t, "parse raw url", err) - u.Path = "/auth/basic/login" - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body)) - assert.Success(t, "new request", err) - - resp, err := http.DefaultClient.Do(req) - assert.Success(t, "do request", err) - assert.Equal(t, "request status 201", http.StatusCreated, resp.StatusCode) - - var tokenResp loginBuiltInAuthResp - err = json.NewDecoder(resp.Body).Decode(&tokenResp) - assert.Success(t, "decode response", err) - - defer resp.Body.Close() - - return tokenResp.SessionToken -} diff --git a/ci/integration/setup_test.go b/ci/integration/setup_test.go deleted file mode 100644 index 45cb7f04..00000000 --- a/ci/integration/setup_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package integration - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/pkg/tcli" -) - -// binpath is populated during package initialization with a path to the coder binary. -var binpath string - -// initialize integration tests by building the coder-cli binary. -func init() { - cwd, err := os.Getwd() - if err != nil { - panic(err) - } - - binpath = filepath.Join(cwd, "bin", "coder") - err = build(binpath) - if err != nil { - panic(err) - } -} - -// build the coder-cli binary and move to the integration testing bin directory. -func build(path string) error { - tar := "coder-cli-linux-amd64.tar.gz" - dir := filepath.Dir(path) - cmd := exec.Command( - "sh", "-c", - fmt.Sprintf( - "cd ../../ && mkdir -p %s && make build/linux && cp ./ci/bin/%s %s/ && tar -xzf %s -C %s", - dir, tar, dir, filepath.Join(dir, tar), dir), - ) - - out, err := cmd.CombinedOutput() - if err != nil { - return xerrors.Errorf("build coder-cli (%v): %w", string(out), err) - } - return nil -} - -// write session tokens to the given container runner. -func headlessLogin(ctx context.Context, t *testing.T, runner *tcli.ContainerRunner) { - creds := login(ctx, t) - cmd := exec.CommandContext(ctx, "sh", "-c", "mkdir -p $HOME/.config/coder && cat > $HOME/.config/coder/session") - - // !IMPORTANT: be careful that this does not appear in logs - cmd.Stdin = strings.NewReader(creds.token) - runner.RunCmd(cmd).Assert(t, - tcli.Success(), - ) - - cmd = exec.CommandContext(ctx, "sh", "-c", "cat > $HOME/.config/coder/url") - cmd.Stdin = strings.NewReader(creds.url) - runner.RunCmd(cmd).Assert(t, - tcli.Success(), - ) -} diff --git a/ci/integration/ssh_test.go b/ci/integration/ssh_test.go deleted file mode 100644 index 5844ca93..00000000 --- a/ci/integration/ssh_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package integration - -import ( - "context" - "testing" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/tcli" -) - -func TestSSH(t *testing.T) { - t.Parallel() - run(t, "ssh-coder-cli-tests", func(t *testing.T, ctx context.Context, c *tcli.ContainerRunner) { - headlessLogin(ctx, t, c) - - // TODO remove this once we can create a workspace if there aren't any - var workspaces []coder.Workspace - c.Run(ctx, "coder workspaces ls --output json").Assert(t, - tcli.Success(), - tcli.StdoutJSONUnmarshal(&workspaces), - ) - - assert := tcli.Success() - - // if we don't have any workspaces, "coder config-ssh" will fail - if len(workspaces) == 0 { - assert = tcli.Error() - } - c.Run(ctx, "coder config-ssh").Assert(t, - assert, - ) - }) -} diff --git a/ci/integration/statictokens_test.go b/ci/integration/statictokens_test.go deleted file mode 100644 index b1de474c..00000000 --- a/ci/integration/statictokens_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package integration - -import ( - "context" - "fmt" - "os" - "os/exec" - "strings" - "testing" - - "cdr.dev/coder-cli/pkg/tcli" -) - -func TestStaticAuth(t *testing.T) { - t.Parallel() - t.Skip() - run(t, "static-auth-test", func(t *testing.T, ctx context.Context, c *tcli.ContainerRunner) { - headlessLogin(ctx, t, c) - - c.Run(ctx, "coder tokens ls").Assert(t, - tcli.Success(), - ) - - var result *tcli.CommandResult - tokenName := randString(5) - c.Run(ctx, "coder tokens create "+tokenName).Assert(t, - tcli.Success(), - tcli.GetResult(&result), - ) - - // remove loging credentials - c.Run(ctx, "rm -rf ~/.config/coder").Assert(t, - tcli.Success(), - ) - - // make requests with token environment variable authentication - cmd := exec.CommandContext(ctx, "sh", "-c", - fmt.Sprintf("export CODER_URL=%s && export CODER_TOKEN=$(cat) && coder workspaces ls", os.Getenv("CODER_URL")), - ) - cmd.Stdin = strings.NewReader(string(result.Stdout)) - c.RunCmd(cmd).Assert(t, - tcli.Success(), - ) - - // should error when the environment variables aren't set - c.Run(ctx, "coder workspaces ls").Assert(t, - tcli.Error(), - ) - }) -} diff --git a/ci/scripts/build.sh b/ci/scripts/build.sh deleted file mode 100755 index 821c062e..00000000 --- a/ci/scripts/build.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash - -# Make pushd and popd silent -pushd() { builtin pushd "$@" >/dev/null; } -popd() { builtin popd >/dev/null; } - -set -euo pipefail - -cd "$(git rev-parse --show-toplevel)/ci/scripts" - -tag="$(git describe --tags)" - -flavor="$GOOS" -if [[ "$GOOS" == "windows" ]]; then - # GOARCH causes bugs with the safeexec package on Windows. - unset GOARCH -else - flavor+="-$GOARCH" -fi -echo "--- building coder-cli for $flavor" - -tmpdir="$(mktemp -d)" -go build -ldflags "-X cdr.dev/coder-cli/internal/version.Version=${tag}" -o "$tmpdir/coder" ../../cmd/coder - -cp ../gon.json $tmpdir/gon.json - -pushd "$tmpdir" -case "$GOOS" in -"windows") - artifact="coder-cli-$GOOS.zip" - mv coder coder.exe - zip "$artifact" coder.exe - ;; -"linux") - artifact="coder-cli-$GOOS-$GOARCH.tar.gz" - tar -czf "$artifact" coder - ;; -"darwin") - if [[ ${CI-} ]]; then - artifact="coder-cli-$GOOS-$GOARCH.zip" - gon -log-level debug ./gon.json - mv coder.zip $artifact - else - artifact="coder-cli-$GOOS-$GOARCH.tar.gz" - tar -czf "$artifact" coder - echo "--- warning: not in ci, skipping signed release of darwin" - fi - ;; -esac -popd - -mkdir -p ../bin -cp "$tmpdir/$artifact" ../bin/$artifact -rm -rf "$tmpdir" diff --git a/ci/scripts/files_changed.sh b/ci/scripts/files_changed.sh deleted file mode 100755 index 490cb5ad..00000000 --- a/ci/scripts/files_changed.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -cd "$(git rev-parse --show-toplevel)" - -if [[ $(git ls-files --other --modified --exclude-standard) ]]; then - echo "Files have changed:" - git ls-files --other --modified --exclude-standard - git -c color.ui=never status - exit 1 -fi diff --git a/ci/scripts/integration.sh b/ci/scripts/integration.sh deleted file mode 100755 index 6f82475c..00000000 --- a/ci/scripts/integration.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -set -eo pipefail - -cd "$(git rev-parse --show-toplevel)" - -echo "--- building integration test image" -docker build -f ./ci/integration/Dockerfile -t coder-cli-integration:latest . - -echo "--- starting integration tests" -go test ./ci/integration -count=1 diff --git a/cmd/coder/main.go b/cmd/coder/main.go deleted file mode 100644 index 93abeb3c..00000000 --- a/cmd/coder/main.go +++ /dev/null @@ -1,56 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "net/http" - _ "net/http/pprof" - "os" - "runtime" - - "cdr.dev/coder-cli/internal/cmd" - "cdr.dev/coder-cli/internal/version" - "cdr.dev/coder-cli/internal/x/xterminal" - "cdr.dev/coder-cli/pkg/clog" -) - -func main() { - ctx, cancel := context.WithCancel(context.Background()) - - // If requested, spin up the pprof webserver. - if os.Getenv("PPROF") != "" { - go func() { - log.Println(http.ListenAndServe("localhost:6060", nil)) - }() - } - - restoreTerminal := func() {} - - // Janky, but SSH on windows sets the output to raw. - // If we set it ourselves, SSH fails because the FD isn't found. - if len(os.Args) >= 2 && os.Args[1] != "tunnel" { - state, err := xterminal.MakeOutputRaw(os.Stdout.Fd()) - if err != nil { - clog.Log(clog.Fatal(fmt.Sprintf("set output to raw: %s", err))) - cancel() - os.Exit(1) - } - restoreTerminal = func() { - // Best effort. Would result in broken terminal on window but nothing we can do about it. - _ = xterminal.Restore(os.Stdout.Fd(), state) - } - } - - app := cmd.Make() - app.Version = fmt.Sprintf("%s %s %s/%s", version.Version, runtime.Version(), runtime.GOOS, runtime.GOARCH) - - if err := app.ExecuteContext(ctx); err != nil { - clog.Log(err) - cancel() - restoreTerminal() - os.Exit(1) - } - cancel() - restoreTerminal() -} diff --git a/coder-sdk/README.md b/coder-sdk/README.md deleted file mode 100644 index b5fbec97..00000000 --- a/coder-sdk/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# coder-sdk - -`coder-sdk` is a Go client library for [Coder](https://coder.com). -It is not yet stable and therefore we do not recommend depending on the current -state of its public APIs. - -## Usage - -```bash -go get cdr.dev/coder-cli/coder-sdk -``` diff --git a/coder-sdk/activity.go b/coder-sdk/activity.go deleted file mode 100644 index 23c75abd..00000000 --- a/coder-sdk/activity.go +++ /dev/null @@ -1,27 +0,0 @@ -package coder - -import ( - "context" - "net/http" -) - -type activityRequest struct { - Source string `json:"source"` - WorkspaceID string `json:"workspace_id"` -} - -// PushActivity pushes CLI activity to Coder. -func (c *DefaultClient) PushActivity(ctx context.Context, source, workspaceID string) error { - resp, err := c.request(ctx, http.MethodPost, "/api/private/metrics/usage/push", activityRequest{ - Source: source, - WorkspaceID: workspaceID, - }) - if err != nil { - return err - } - - if resp.StatusCode != http.StatusOK { - return NewHTTPError(resp) - } - return nil -} diff --git a/coder-sdk/activity_test.go b/coder-sdk/activity_test.go deleted file mode 100644 index ff3083a3..00000000 --- a/coder-sdk/activity_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package coder_test - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/coder-sdk" -) - -func TestPushActivity(t *testing.T) { - t.Parallel() - - const source = "test" - const workspaceID = "602d377a-e6b8d763cae7561885c5f1b2" - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "PushActivity is a POST", http.MethodPost, r.Method) - assert.Equal(t, "URL matches", "/api/private/metrics/usage/push", r.URL.Path) - - expected := map[string]interface{}{ - "source": source, - "workspace_id": workspaceID, - } - var request map[string]interface{} - err := json.NewDecoder(r.Body).Decode(&request) - assert.Success(t, "error decoding JSON", err) - assert.Equal(t, "unexpected request data", expected, request) - - w.WriteHeader(http.StatusOK) - })) - t.Cleanup(func() { - server.Close() - }) - - u, err := url.Parse(server.URL) - assert.Success(t, "failed to parse test server URL", err) - - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - Token: "SwdcSoq5Jc-0C1r8wfwm7h6h9i0RDk7JT", - }) - assert.Success(t, "failed to create coder.Client", err) - - err = client.PushActivity(context.Background(), source, workspaceID) - assert.Success(t, "expected successful response from PushActivity", err) -} diff --git a/coder-sdk/agent.go b/coder-sdk/agent.go deleted file mode 100644 index 29052f11..00000000 --- a/coder-sdk/agent.go +++ /dev/null @@ -1,22 +0,0 @@ -package coder - -import ( - "context" - "fmt" - "net/http" -) - -// UpdateLastConnectionAt updates the last connection at attribute of a workspace. -func (c *DefaultClient) UpdateLastConnectionAt(ctx context.Context, workspaceID string) error { - reqURL := fmt.Sprintf("/api/private/envagent/%s/update-last-connection-at", workspaceID) - resp, err := c.request(ctx, http.MethodPost, reqURL, nil) - if err != nil { - return err - } - - if resp.StatusCode != http.StatusOK { - return NewHTTPError(resp) - } - - return nil -} diff --git a/coder-sdk/client.go b/coder-sdk/client.go deleted file mode 100644 index 1cba0fcb..00000000 --- a/coder-sdk/client.go +++ /dev/null @@ -1,126 +0,0 @@ -package coder - -import ( - "context" - "errors" - "net/http" - "net/url" - "time" - - "golang.org/x/xerrors" -) - -// ensure that DefaultClient implements Client. -var _ = Client(&DefaultClient{}) - -// Me is the user ID of the authenticated user. -const Me = "me" - -// ClientOptions contains options for the Coder SDK Client. -type ClientOptions struct { - // BaseURL is the root URL of the Coder installation (required). - BaseURL *url.URL - - // Client is the http.Client to use for requests (optional). - // - // If omitted, the http.DefaultClient will be used. - HTTPClient *http.Client - - // Token is the API Token used to authenticate (optional). - // - // If Token is provided, the DefaultClient will use it to authenticate. - // If it is not provided, the client requires another type of - // credential, such as an Email/Password pair. - Token string - - // Email used to authenticate with Coder. - // - // If you supply an Email and Password pair, NewClient will exchange - // these credentials for a Token during initialization. This is only - // applicable for the built-in authentication provider. The client will - // not retain these credentials in memory after NewClient returns. - Email string - - // Password used to authenticate with Coder. - // - // If you supply an Email and Password pair, NewClient will exchange - // these credentials for a Token during initialization. This is only - // applicable for the built-in authentication provider. The client will - // not retain these credentials in memory after NewClient returns. - Password string -} - -// NewClient creates a new default Coder SDK client. -func NewClient(opts ClientOptions) (*DefaultClient, error) { - httpClient := opts.HTTPClient - if httpClient == nil { - httpClient = http.DefaultClient - } - - if opts.BaseURL == nil { - return nil, errors.New("the BaseURL parameter is required") - } - - token := opts.Token - if token == "" { - if opts.Email == "" || opts.Password == "" { - return nil, errors.New("either an API Token or email/password pair are required") - } - - // Exchange the username/password for a token. - // We apply a default timeout of 5 seconds here. - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - - resp, err := LoginWithPassword(ctx, httpClient, opts.BaseURL, &LoginRequest{ - Email: opts.Email, - Password: opts.Password, - }) - if err != nil { - return nil, xerrors.Errorf("failed to login with email/password: %w", err) - } - - token = resp.SessionToken - if token == "" { - return nil, errors.New("server returned an empty session token") - } - } - - // TODO: add basic validation to make sure the token looks OK. - - client := &DefaultClient{ - baseURL: opts.BaseURL, - httpClient: httpClient, - token: token, - } - - return client, nil -} - -// DefaultClient is the default implementation of the coder.Client -// interface. -// -// The empty value is meaningless and the fields are unexported; -// use NewClient to create an instance. -type DefaultClient struct { - // baseURL is the URL (scheme, hostname/IP address, port, - // path prefix of the Coder installation) - baseURL *url.URL - - // httpClient is the http.Client used to issue requests. - httpClient *http.Client - - // token is the API Token credential. - token string -} - -// Token returns the API Token used to authenticate. -func (c *DefaultClient) Token() string { - return c.token -} - -// BaseURL returns the BaseURL configured for this Client. -func (c *DefaultClient) BaseURL() url.URL { - return *c.baseURL -} diff --git a/coder-sdk/client_test.go b/coder-sdk/client_test.go deleted file mode 100644 index 732678d0..00000000 --- a/coder-sdk/client_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package coder_test - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/coder-sdk" -) - -func TestAuthentication(t *testing.T) { - t.Parallel() - - const token = "g4mtIPUaKt-pPl9Q0xmgKs7acSypHt4Jf" - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - gotToken := r.Header.Get("Session-Token") - assert.Equal(t, "token does not match", token, gotToken) - - w.WriteHeader(http.StatusServiceUnavailable) - })) - t.Cleanup(func() { - server.Close() - }) - - u, err := url.Parse(server.URL) - assert.Success(t, "failed to parse test server URL", err) - - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - Token: token, - }) - assert.Success(t, "failed to create coder.Client", err) - - assert.Equal(t, "expected Token to match", token, client.Token()) - assert.Equal(t, "expected BaseURL to match", *u, client.BaseURL()) - - _, err = client.APIVersion(context.Background()) - assert.Success(t, "failed to get API version information", err) -} - -func TestPasswordAuthentication(t *testing.T) { - t.Parallel() - - const email = "user@coder.com" - const password = "coder4all" - const token = "g4mtIPUaKt-pPl9Q0xmgKs7acSypHt4Jf" - - mux := http.NewServeMux() - mux.HandleFunc("/auth/basic/login", func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "login is a POST", http.MethodPost, r.Method) - - expected := map[string]interface{}{ - "email": email, - "password": password, - } - var request map[string]interface{} - err := json.NewDecoder(r.Body).Decode(&request) - assert.Success(t, "error decoding JSON", err) - assert.Equal(t, "unexpected request data", expected, request) - - response := map[string]interface{}{ - "session_token": token, - } - - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(response) - assert.Success(t, "error encoding JSON", err) - }) - mux.HandleFunc("/api/v0/users/me", func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "Users is a GET", http.MethodGet, r.Method) - - gotToken := r.Header.Get("Session-Token") - assert.Equal(t, "expected session token to match return of login", token, gotToken) - - user := map[string]interface{}{ - "id": "default", - "email": email, - "username": "charlie", - "name": "Charlie Root", - "roles": []coder.Role{coder.SiteAdmin}, - "temporary_password": false, - "login_type": coder.LoginTypeBuiltIn, - "key_regenerated_at": time.Now(), - "created_at": time.Now(), - "updated_at": time.Now(), - } - - w.WriteHeader(http.StatusOK) - err := json.NewEncoder(w).Encode(user) - assert.Success(t, "error encoding JSON", err) - }) - server := httptest.NewTLSServer(mux) - t.Cleanup(func() { - server.Close() - }) - - u, err := url.Parse(server.URL) - assert.Success(t, "failed to parse test server URL", err) - assert.Equal(t, "expected HTTPS base URL", "https", u.Scheme) - - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - HTTPClient: server.Client(), - Email: email, - Password: password, - }) - assert.Success(t, "failed to create Client", err) - assert.Equal(t, "expected token to match", token, client.Token()) - - user, err := client.Me(context.Background()) - assert.Success(t, "failed to get information about current user", err) - assert.Equal(t, "expected test user", email, user.Email) -} - -func TestContextRoot(t *testing.T) { - t.Parallel() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "Users is a GET", http.MethodGet, r.Method) - assert.Equal(t, "expected context root", "/context-root/api/v0/users", r.URL.Path) - - w.WriteHeader(http.StatusServiceUnavailable) - })) - t.Cleanup(func() { - server.Close() - }) - - contextRoots := []string{ - "/context-root", - "/context-root/", - } - - u, err := url.Parse(server.URL) - assert.Success(t, "failed to parse test server URL", err) - - for _, prefix := range contextRoots { - u.Path = prefix - - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - Token: "FrOgA6xhpM-p5nTfsupmvzYJA6DJSOUoE", - }) - assert.Success(t, "failed to create coder.Client", err) - - _, err = client.Users(context.Background()) - assert.Error(t, "expected 503 error", err) - } -} diff --git a/coder-sdk/config.go b/coder-sdk/config.go deleted file mode 100644 index a38f2357..00000000 --- a/coder-sdk/config.go +++ /dev/null @@ -1,226 +0,0 @@ -package coder - -import ( - "context" - "net/http" -) - -// AuthProviderType is an enum of each valid auth provider. -type AuthProviderType string - -// AuthProviderType enum. -const ( - AuthProviderBuiltIn AuthProviderType = "built-in" - AuthProviderSAML AuthProviderType = "saml" - AuthProviderOIDC AuthProviderType = "oidc" -) - -// ConfigAuth describes the authentication configuration for a Coder -// deployment. -type ConfigAuth struct { - ProviderType *AuthProviderType `json:"provider_type"` - OIDC *ConfigOIDC `json:"oidc"` - SAML *ConfigSAML `json:"saml"` -} - -// ConfigOIDC describes the OIDC configuration for single-signon support in -// Coder. -type ConfigOIDC struct { - ClientID *string `json:"client_id"` - ClientSecret *string `json:"client_secret"` - Issuer *string `json:"issuer"` -} - -// ConfigSAML describes the SAML configuration values. -type ConfigSAML struct { - IdentityProviderMetadataURL *string `json:"idp_metadata_url"` - SignatureAlgorithm *string `json:"signature_algorithm"` - NameIDFormat *string `json:"name_id_format"` - PrivateKey *string `json:"private_key"` - PublicKeyCertificate *string `json:"public_key_certificate"` -} - -// ConfigOAuthBitbucketServer describes the Bitbucket integration configuration -// for a Coder deployment. -type ConfigOAuthBitbucketServer struct { - BaseURL string `json:"base_url" diff:"oauth.bitbucket_server.base_url"` -} - -// ConfigOAuthGitHub describes the Github integration configuration for a Coder -// deployment. -type ConfigOAuthGitHub struct { - BaseURL string `json:"base_url"` - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` -} - -// ConfigOAuthGitLab describes the GitLab integration configuration for a Coder -// deployment. -type ConfigOAuthGitLab struct { - BaseURL string `json:"base_url"` - ClientID string `json:"client_id" ` - ClientSecret string `json:"client_secret"` -} - -// ConfigOAuth describes the aggregate git integration configuration for a -// Coder deployment. -type ConfigOAuth struct { - BitbucketServer ConfigOAuthBitbucketServer `json:"bitbucket_server"` - GitHub ConfigOAuthGitHub `json:"github"` - GitLab ConfigOAuthGitLab `json:"gitlab"` -} - -// SiteConfigAuth fetches the sitewide authentication configuration. -func (c *DefaultClient) SiteConfigAuth(ctx context.Context) (*ConfigAuth, error) { - var conf ConfigAuth - if err := c.requestBody(ctx, http.MethodGet, "/api/private/auth/config", nil, &conf); err != nil { - return nil, err - } - return &conf, nil -} - -// PutSiteConfigAuth sets the sitewide authentication configuration. -func (c *DefaultClient) PutSiteConfigAuth(ctx context.Context, req ConfigAuth) error { - return c.requestBody(ctx, http.MethodPut, "/api/private/auth/config", req, nil) -} - -// SiteConfigOAuth fetches the sitewide git provider OAuth configuration. -func (c *DefaultClient) SiteConfigOAuth(ctx context.Context) (*ConfigOAuth, error) { - var conf ConfigOAuth - if err := c.requestBody(ctx, http.MethodGet, "/api/private/oauth/config", nil, &conf); err != nil { - return nil, err - } - return &conf, nil -} - -// PutSiteConfigOAuth sets the sitewide git provider OAuth configuration. -func (c *DefaultClient) PutSiteConfigOAuth(ctx context.Context, req ConfigOAuth) error { - return c.requestBody(ctx, http.MethodPut, "/api/private/oauth/config", req, nil) -} - -type configSetupMode struct { - SetupMode bool `json:"setup_mode"` -} - -// SiteSetupModeEnabled fetches the current setup_mode state of a Coder deployment. -func (c *DefaultClient) SiteSetupModeEnabled(ctx context.Context) (bool, error) { - var conf configSetupMode - if err := c.requestBody(ctx, http.MethodGet, "/api/private/config/setup-mode", nil, &conf); err != nil { - return false, err - } - return conf.SetupMode, nil -} - -// ExtensionMarketplaceType is an enum of the valid extension marketplace configurations. -type ExtensionMarketplaceType string - -// ExtensionMarketplaceType enum. -const ( - ExtensionMarketplaceInternal ExtensionMarketplaceType = "internal" - ExtensionMarketplaceCustom ExtensionMarketplaceType = "custom" - ExtensionMarketplacePublic ExtensionMarketplaceType = "public" -) - -// MarketplaceExtensionPublicURL is the URL of the coder.com public marketplace that serves open source Code OSS extensions. -const MarketplaceExtensionPublicURL = "https://extensions.coder.com/api" - -// ConfigExtensionMarketplace describes the sitewide extension marketplace configuration. -type ConfigExtensionMarketplace struct { - URL string `json:"url"` - Type ExtensionMarketplaceType `json:"type"` -} - -// SiteConfigExtensionMarketplace fetches the extension marketplace configuration. -func (c *DefaultClient) SiteConfigExtensionMarketplace(ctx context.Context) (*ConfigExtensionMarketplace, error) { - var conf ConfigExtensionMarketplace - if err := c.requestBody(ctx, http.MethodGet, "/api/private/extensions/config", nil, &conf); err != nil { - return nil, err - } - return &conf, nil -} - -// PutSiteConfigExtensionMarketplace sets the extension marketplace configuration. -func (c *DefaultClient) PutSiteConfigExtensionMarketplace(ctx context.Context, req ConfigExtensionMarketplace) error { - return c.requestBody(ctx, http.MethodPut, "/api/private/extensions/config", req, nil) -} - -// ConfigWorkspaces is the site configuration for workspace attributes. -type ConfigWorkspaces struct { - GPUVendor string `json:"gpu_vendor"` - EnableContainerVMs bool `json:"enable_container_vms"` - EnableWorkspacesAsCode bool `json:"enable_workspaces_as_code"` -} - -// SiteConfigWorkspaces fetches the workspace configuration. -func (c *DefaultClient) SiteConfigWorkspaces(ctx context.Context) (*ConfigWorkspaces, error) { - var conf ConfigWorkspaces - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/workspaces/config", nil, &conf); err != nil { - return nil, err - } - return &conf, nil -} - -// PutSiteConfigWorkspaces sets the workspace configuration. -func (c *DefaultClient) PutSiteConfigWorkspaces(ctx context.Context, req ConfigWorkspaces) error { - return c.requestBody(ctx, http.MethodPut, "/api/v0/workspaces/config", req, nil) -} - -type ConfigDormancy struct { - // UserDormancyThresholdDays is not currently updatable. - // UserDormancyThresholdDays int `json:"user_dormancy_threshold_days"` - UserDeletionThresholdDays int `json:"user_deletion_threshold_days"` -} - -// SiteConfigDormancy fetches the dormancy configuration. -func (c *DefaultClient) SiteConfigDormancy(ctx context.Context) (*ConfigDormancy, error) { - var conf ConfigDormancy - if err := c.requestBody(ctx, http.MethodGet, "/api/private/dormancy/config", nil, &conf); err != nil { - return nil, err - } - return &conf, nil -} - -// PutSiteConfigDormancy sets the dormancy configuration. -func (c *DefaultClient) PutSiteConfigDormancy(ctx context.Context, req ConfigDormancy) error { - return c.requestBody(ctx, http.MethodPut, "/api/private/dormancy/config", req, nil) -} - -type ConfigDevURLAccess struct { - Private bool `json:"private"` - Org bool `json:"org"` - Authed bool `json:"authed"` - Public bool `json:"public"` -} - -// SiteConfigDevURLAccess fetches the DevURL access configuration. -func (c *DefaultClient) SiteConfigDevURLAccess(ctx context.Context) (*ConfigDevURLAccess, error) { - var conf ConfigDevURLAccess - if err := c.requestBody(ctx, http.MethodGet, "/api/private/devurls/config", nil, &conf); err != nil { - return nil, err - } - return &conf, nil -} - -// PutSiteConfigDevURLAccess sets the DevURL access configuration. -func (c *DefaultClient) PutSiteConfigDevURLAccess(ctx context.Context, req ConfigDevURLAccess) error { - return c.requestBody(ctx, http.MethodPut, "/api/private/devurls/config", req, nil) -} - -// ConfigSSHSettings is the site configuration for SSH. -type ConfigSSHSettings struct { - KeygenAlgorithm string `json:"keygen_algorithm"` -} - -// SiteConfigSSHSettings fetches the SSH configuration. -func (c *DefaultClient) SiteConfigSSHSettings(ctx context.Context) (*ConfigSSHSettings, error) { - var conf ConfigSSHSettings - if err := c.requestBody(ctx, http.MethodGet, "/api/private/ssh/config", nil, &conf); err != nil { - return nil, err - } - return &conf, nil -} - -// PutSiteConfigSSHSettings sets the SSH configuration. -func (c *DefaultClient) PutSiteConfigSSHSettings(ctx context.Context, req ConfigSSHSettings) error { - return c.requestBody(ctx, http.MethodPut, "/api/private/ssh/config", req, nil) -} diff --git a/coder-sdk/devurl.go b/coder-sdk/devurl.go deleted file mode 100644 index af6cf64f..00000000 --- a/coder-sdk/devurl.go +++ /dev/null @@ -1,63 +0,0 @@ -package coder - -import ( - "context" - "fmt" - "net/http" -) - -// DevURL is the parsed json response record for a devURL from cemanager. -type DevURL struct { - ID string `json:"id" table:"-"` - URL string `json:"url" table:"URL"` - Port int `json:"port" table:"Port"` - Access string `json:"access" table:"Access"` - Name string `json:"name" table:"Name"` - Scheme string `json:"scheme" table:"Scheme"` -} - -type delDevURLRequest struct { - WorkspaceID string `json:"workspace_id"` - DevURLID string `json:"url_id"` -} - -// DeleteDevURL deletes the specified devurl. -func (c *DefaultClient) DeleteDevURL(ctx context.Context, workspaceID, urlID string) error { - reqURL := fmt.Sprintf("/api/v0/workspaces/%s/devurls/%s", workspaceID, urlID) - - return c.requestBody(ctx, http.MethodDelete, reqURL, delDevURLRequest{ - WorkspaceID: workspaceID, - DevURLID: urlID, - }, nil) -} - -// CreateDevURLReq defines the request parameters for creating a new DevURL. -type CreateDevURLReq struct { - WorkspaceID string `json:"workspace_id"` - Port int `json:"port"` - Access string `json:"access"` - Name string `json:"name"` - Scheme string `json:"scheme"` -} - -// CreateDevURL inserts a new dev URL for the authenticated user. -func (c *DefaultClient) CreateDevURL(ctx context.Context, workspaceID string, req CreateDevURLReq) error { - return c.requestBody(ctx, http.MethodPost, "/api/v0/workspaces/"+workspaceID+"/devurls", req, nil) -} - -// DevURLs fetches the Dev URLs for a given workspace. -func (c *DefaultClient) DevURLs(ctx context.Context, workspaceID string) ([]DevURL, error) { - var devurls []DevURL - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/workspaces/"+workspaceID+"/devurls", nil, &devurls); err != nil { - return nil, err - } - return devurls, nil -} - -// PutDevURLReq defines the request parameters for overwriting a DevURL. -type PutDevURLReq CreateDevURLReq - -// PutDevURL updates an existing devurl for the authenticated user. -func (c *DefaultClient) PutDevURL(ctx context.Context, workspaceID, urlID string, req PutDevURLReq) error { - return c.requestBody(ctx, http.MethodPut, "/api/v0/workspaces/"+workspaceID+"/devurls/"+urlID, req, nil) -} diff --git a/coder-sdk/doc.go b/coder-sdk/doc.go deleted file mode 100644 index 5fe3bd46..00000000 --- a/coder-sdk/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package coder provides simple APIs for integrating Go applications with Coder. -package coder diff --git a/coder-sdk/error.go b/coder-sdk/error.go deleted file mode 100644 index 9e1645d0..00000000 --- a/coder-sdk/error.go +++ /dev/null @@ -1,92 +0,0 @@ -package coder - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - - "golang.org/x/xerrors" -) - -// ErrNotFound describes an error case in which the requested resource could not be found. -var ErrNotFound = xerrors.New("resource not found") - -// ErrPermissions describes an error case in which the requester has insufficient permissions to access the requested resource. -var ErrPermissions = xerrors.New("insufficient permissions") - -// ErrAuthentication describes the error case in which the requester has invalid authentication. -var ErrAuthentication = xerrors.New("invalid authentication") - -// APIError is the expected payload format for API errors. -type APIError struct { - Err APIErrorMsg `json:"error"` -} - -// APIErrorMsg contains the rich error information returned by API errors. -type APIErrorMsg struct { - Msg string `json:"msg"` - Code string `json:"code"` - Details json.RawMessage `json:"details"` -} - -// NewHTTPError reads the response body and stores metadata -// about the response in order to be unpacked into -// an *APIError. -func NewHTTPError(resp *http.Response) *HTTPError { - var buf bytes.Buffer - _, err := io.Copy(&buf, resp.Body) - if err != nil { - return &HTTPError{ - cachedErr: err, - } - } - return &HTTPError{ - url: resp.Request.URL.String(), - statusCode: resp.StatusCode, - body: buf.Bytes(), - } -} - -// HTTPError represents an error from the Coder API. -type HTTPError struct { - url string - statusCode int - body []byte - cached *APIError - cachedErr error -} - -// Payload decode the response body into the standard error structure. The `details` -// section is stored as a raw json, and type depends on the `code` field. -func (e *HTTPError) Payload() (*APIError, error) { - var msg APIError - if e.cached != nil || e.cachedErr != nil { - return e.cached, e.cachedErr - } - - // Try to decode the payload as an error, if it fails or if there is no error message, - // return the response URL with the status. - if err := json.Unmarshal(e.body, &msg); err != nil { - e.cachedErr = err - return nil, err - } - - e.cached = &msg - return &msg, nil -} - -func (e *HTTPError) StatusCode() int { - return e.statusCode -} - -func (e *HTTPError) Error() string { - apiErr, err := e.Payload() - if err != nil || apiErr.Err.Msg == "" { - return fmt.Sprintf("%s: %d %s", e.url, e.statusCode, http.StatusText(e.statusCode)) - } - - // If the payload was a in the expected error format with a message, include it. - return apiErr.Err.Msg -} diff --git a/coder-sdk/image.go b/coder-sdk/image.go deleted file mode 100644 index c5101c87..00000000 --- a/coder-sdk/image.go +++ /dev/null @@ -1,101 +0,0 @@ -package coder - -import ( - "context" - "net/http" - "net/url" - "time" -) - -// Image describes a Coder Image. -type Image struct { - ID string `json:"id" table:"-"` - OrganizationID string `json:"organization_id" table:"-"` - Repository string `json:"repository" table:"Repository"` - Description string `json:"description" table:"-"` - URL string `json:"url" table:"-"` // User-supplied URL for image. - Registry *Registry `json:"registry" table:"-"` - DefaultTag *ImageTag `json:"default_tag" table:"DefaultTag"` - DefaultCPUCores float32 `json:"default_cpu_cores" table:"DefaultCPUCores"` - DefaultMemoryGB float32 `json:"default_memory_gb" table:"DefaultMemoryGB"` - DefaultDiskGB int `json:"default_disk_gb" table:"DefaultDiskGB"` - Deprecated bool `json:"deprecated" table:"-"` - CreatedAt time.Time `json:"created_at" table:"-"` - UpdatedAt time.Time `json:"updated_at" table:"-"` -} - -// NewRegistryRequest describes a docker registry used in importing an image. -type NewRegistryRequest struct { - FriendlyName string `json:"friendly_name"` - Registry string `json:"registry"` - Username string `json:"username"` - Password string `json:"password"` -} - -// ImportImageReq is used to import new images and registries into Coder. -type ImportImageReq struct { - RegistryID *string `json:"registry_id"` // Used to import images to existing registries. - NewRegistry *NewRegistryRequest `json:"new_registry"` // Used when adding a new registry. - Repository string `json:"repository"` // Refers to the image. Ex: "codercom/ubuntu". - OrgID string `json:"org_id"` - Tag string `json:"tag"` - DefaultCPUCores float32 `json:"default_cpu_cores"` - DefaultMemoryGB int `json:"default_memory_gb"` - DefaultDiskGB int `json:"default_disk_gb"` - Description string `json:"description"` - URL string `json:"url"` -} - -// UpdateImageReq defines the requests parameters for a partial update of an image resource. -type UpdateImageReq struct { - DefaultCPUCores *float32 `json:"default_cpu_cores"` - DefaultMemoryGB *float32 `json:"default_memory_gb"` - DefaultDiskGB *int `json:"default_disk_gb"` - Description *string `json:"description"` - URL *string `json:"url"` - Deprecated *bool `json:"deprecated"` - DefaultTag *string `json:"default_tag"` -} - -// ImportImage creates a new image and optionally a new registry. -func (c *DefaultClient) ImportImage(ctx context.Context, req ImportImageReq) (*Image, error) { - var img Image - if err := c.requestBody(ctx, http.MethodPost, "/api/v0/images", req, &img); err != nil { - return nil, err - } - return &img, nil -} - -// ImageByID returns an image entity, fetched by its ID. -func (c *DefaultClient) ImageByID(ctx context.Context, id string) (*Image, error) { - var img Image - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/images/"+id, nil, &img); err != nil { - return nil, err - } - return &img, nil -} - -// OrganizationImages returns all of the images imported for orgID. -func (c *DefaultClient) OrganizationImages(ctx context.Context, orgID string) ([]Image, error) { - var ( - imgs []Image - query = url.Values{} - ) - - query.Set("org", orgID) - - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/images", nil, &imgs, withQueryParams(query)); err != nil { - return nil, err - } - return imgs, nil -} - -// UpdateImage applies a partial update to an image resource. -func (c *DefaultClient) UpdateImage(ctx context.Context, imageID string, req UpdateImageReq) error { - return c.requestBody(ctx, http.MethodPatch, "/api/v0/images/"+imageID, req, nil) -} - -// UpdateImageTags refreshes the latest digests for all tags of the image. -func (c *DefaultClient) UpdateImageTags(ctx context.Context, imageID string) error { - return c.requestBody(ctx, http.MethodPost, "/api/v0/images/"+imageID+"/tags/update", nil, nil) -} diff --git a/coder-sdk/interface.go b/coder-sdk/interface.go deleted file mode 100644 index 6394b88b..00000000 --- a/coder-sdk/interface.go +++ /dev/null @@ -1,260 +0,0 @@ -package coder - -import ( - "context" - "net/url" - - "cdr.dev/wsep" - "github.com/pion/webrtc/v3" - "nhooyr.io/websocket" -) - -// Client wraps the Coder HTTP API. -// This is an interface to allow for mocking of coder-sdk client usage. -type Client interface { - // PushActivity pushes CLI activity to Coder. - PushActivity(ctx context.Context, source, workspaceID string) error - - // Me gets the details of the authenticated user. - Me(ctx context.Context) (*User, error) - - // UserByID get the details of a user by their id. - UserByID(ctx context.Context, id string) (*User, error) - - // SSHKey gets the current SSH kepair of the authenticated user. - SSHKey(ctx context.Context) (*SSHKey, error) - - // Users gets the list of user accounts. - Users(ctx context.Context) ([]User, error) - - // UserByEmail gets a user by email. - UserByEmail(ctx context.Context, email string) (*User, error) - - // UpdateUser applyes the partial update to the given user. - UpdateUser(ctx context.Context, userID string, req UpdateUserReq) error - - // UpdateUXState applies a partial update of the user's UX State. - UpdateUXState(ctx context.Context, userID string, uxsPartial map[string]interface{}) error - - // CreateUser creates a new user account. - CreateUser(ctx context.Context, req CreateUserReq) error - - // DeleteUser deletes a user account. - DeleteUser(ctx context.Context, userID string) error - - // SiteConfigAuth fetches the sitewide authentication configuration. - SiteConfigAuth(ctx context.Context) (*ConfigAuth, error) - - // PutSiteConfigAuth sets the sitewide authentication configuration. - PutSiteConfigAuth(ctx context.Context, req ConfigAuth) error - - // SiteConfigOAuth fetches the sitewide git provider OAuth configuration. - SiteConfigOAuth(ctx context.Context) (*ConfigOAuth, error) - - // PutSiteConfigOAuth sets the sitewide git provider OAuth configuration. - PutSiteConfigOAuth(ctx context.Context, req ConfigOAuth) error - - // SiteSetupModeEnabled fetches the current setup_mode state of a Coder deployment. - SiteSetupModeEnabled(ctx context.Context) (bool, error) - - // SiteConfigExtensionMarketplace fetches the extension marketplace configuration. - SiteConfigExtensionMarketplace(ctx context.Context) (*ConfigExtensionMarketplace, error) - - // PutSiteConfigExtensionMarketplace sets the extension marketplace configuration. - PutSiteConfigExtensionMarketplace(ctx context.Context, req ConfigExtensionMarketplace) error - - // SiteConfigWorkspaces fetches the workspace configuration. - SiteConfigWorkspaces(ctx context.Context) (*ConfigWorkspaces, error) - - // DeleteDevURL deletes the specified devurl. - DeleteDevURL(ctx context.Context, workspaceID, urlID string) error - - // CreateDevURL inserts a new devurl for the authenticated user. - CreateDevURL(ctx context.Context, workspaceID string, req CreateDevURLReq) error - - // DevURLs fetches the Dev URLs for a given workspace. - DevURLs(ctx context.Context, workspaceID string) ([]DevURL, error) - - // PutDevURL updates an existing devurl for the authenticated user. - PutDevURL(ctx context.Context, workspaceID, urlID string, req PutDevURLReq) error - - // CreateWorkspace sends a request to create a workspace. - CreateWorkspace(ctx context.Context, req CreateWorkspaceRequest) (*Workspace, error) - - // ParseTemplate parses a template config. It support both remote repositories and local files. - // If a local file is specified then all other values in the request are ignored. - ParseTemplate(ctx context.Context, req ParseTemplateRequest) (*TemplateVersion, error) - - // CreateWorkspaceFromRepo sends a request to create a workspace from a repository. - CreateWorkspaceFromRepo(ctx context.Context, orgID string, req TemplateVersion) (*Workspace, error) - - // Workspaces lists workspaces returned by the given filter. - Workspaces(ctx context.Context) ([]Workspace, error) - - // UserWorkspacesByOrganization gets the list of workspaces owned by the given user. - UserWorkspacesByOrganization(ctx context.Context, userID, orgID string) ([]Workspace, error) - - // DeleteWorkspace deletes the workspace. - DeleteWorkspace(ctx context.Context, workspaceID string) error - - // StopWorkspace stops the workspace. - StopWorkspace(ctx context.Context, workspaceID string) error - - // RebuildWorkspace requests that the given workspaceID is rebuilt with no changes to its specification. - RebuildWorkspace(ctx context.Context, workspaceID string) error - - // EditWorkspace modifies the workspace specification and initiates a rebuild. - EditWorkspace(ctx context.Context, workspaceID string, req UpdateWorkspaceReq) error - - // DialWsep dials a workspace's command execution interface - // See https://github.com/cdr/wsep for details. - DialWsep(ctx context.Context, baseURL *url.URL, workspaceID string) (*websocket.Conn, error) - - // DialExecutor gives a remote execution interface for performing commands inside a workspace. - DialExecutor(ctx context.Context, baseURL *url.URL, workspaceID string) (wsep.Execer, error) - - // DialIDEStatus opens a websocket connection for cpu load metrics on the workspace. - DialIDEStatus(ctx context.Context, baseURL *url.URL, workspaceID string) (*websocket.Conn, error) - - // DialWorkspaceBuildLog opens a websocket connection for the workspace build log messages. - DialWorkspaceBuildLog(ctx context.Context, workspaceID string) (*websocket.Conn, error) - - // FollowWorkspaceBuildLog trails the build log of a Coder workspace. - FollowWorkspaceBuildLog(ctx context.Context, workspaceID string) (<-chan BuildLogFollowMsg, error) - - // DialWorkspaceStats opens a websocket connection for workspace stats. - DialWorkspaceStats(ctx context.Context, workspaceID string) (*websocket.Conn, error) - - // DialResourceLoad opens a websocket connection for cpu load metrics on the workspace. - DialResourceLoad(ctx context.Context, workspaceID string) (*websocket.Conn, error) - - // WaitForWorkspaceReady will watch the build log and return when done. - WaitForWorkspaceReady(ctx context.Context, workspaceID string) error - - // WorkspaceByID get the details of a workspace by its id. - WorkspaceByID(ctx context.Context, id string) (*Workspace, error) - - // WorkspacesByWorkspaceProvider returns workspaces that belong to a particular workspace provider. - WorkspacesByWorkspaceProvider(ctx context.Context, wpID string) ([]Workspace, error) - - // ImportImage creates a new image and optionally a new registry. - ImportImage(ctx context.Context, req ImportImageReq) (*Image, error) - - // ImageByID returns an image entity, fetched by its ID. - ImageByID(ctx context.Context, id string) (*Image, error) - - // OrganizationImages returns all of the images imported for orgID. - OrganizationImages(ctx context.Context, orgID string) ([]Image, error) - - // UpdateImage applies a partial update to an image resource. - UpdateImage(ctx context.Context, imageID string, req UpdateImageReq) error - - // UpdateImageTags refreshes the latest digests for all tags of the image. - UpdateImageTags(ctx context.Context, imageID string) error - - // Organizations gets all Organizations. - Organizations(ctx context.Context) ([]Organization, error) - - // OrganizationByID get the Organization by its ID. - OrganizationByID(ctx context.Context, orgID string) (*Organization, error) - - // OrganizationMembers get all members of the given organization. - OrganizationMembers(ctx context.Context, orgID string) ([]OrganizationUser, error) - - // UpdateOrganization applys a partial update of an Organization resource. - UpdateOrganization(ctx context.Context, orgID string, req UpdateOrganizationReq) error - - // CreateOrganization creates a new Organization in Coder. - CreateOrganization(ctx context.Context, req CreateOrganizationReq) error - - // DeleteOrganization deletes an organization. - DeleteOrganization(ctx context.Context, orgID string) error - - // Registries fetches all registries in an organization. - Registries(ctx context.Context, orgID string) ([]Registry, error) - - // RegistryByID fetches a registry resource by its ID. - RegistryByID(ctx context.Context, registryID string) (*Registry, error) - - // UpdateRegistry applies a partial update to a registry resource. - UpdateRegistry(ctx context.Context, registryID string, req UpdateRegistryReq) error - - // DeleteRegistry deletes a registry resource by its ID. - DeleteRegistry(ctx context.Context, registryID string) error - - // CreateImageTag creates a new image tag resource. - CreateImageTag(ctx context.Context, imageID string, req CreateImageTagReq) (*ImageTag, error) - - // DeleteImageTag deletes an image tag resource. - DeleteImageTag(ctx context.Context, imageID, tag string) error - - // ImageTags fetch all image tags. - ImageTags(ctx context.Context, imageID string) ([]ImageTag, error) - - // ImageTagByID fetch an image tag by ID. - ImageTagByID(ctx context.Context, imageID, tagID string) (*ImageTag, error) - - // CreateAPIToken creates a new APIToken for making authenticated requests to Coder. - CreateAPIToken(ctx context.Context, userID string, req CreateAPITokenReq) (string, error) - - // APITokens fetches all APITokens owned by the given user. - APITokens(ctx context.Context, userID string) ([]APIToken, error) - - // APITokenByID fetches the metadata for a given APIToken. - APITokenByID(ctx context.Context, userID, tokenID string) (*APIToken, error) - - // DeleteAPIToken deletes an APIToken. - DeleteAPIToken(ctx context.Context, userID, tokenID string) error - - // RegenerateAPIToken regenerates the given APIToken and returns the new value. - RegenerateAPIToken(ctx context.Context, userID, tokenID string) (string, error) - - // APIVersion parses the coder-version http header from an authenticated request. - APIVersion(ctx context.Context) (string, error) - - // WorkspaceProviderByID fetches a workspace provider entity by its unique ID. - WorkspaceProviderByID(ctx context.Context, id string) (*KubernetesProvider, error) - - // WorkspaceProviders fetches all workspace providers known to the Coder control plane. - WorkspaceProviders(ctx context.Context) (*WorkspaceProviders, error) - - // CreateWorkspaceProvider creates a new WorkspaceProvider entity. - CreateWorkspaceProvider(ctx context.Context, req CreateWorkspaceProviderReq) (*CreateWorkspaceProviderRes, error) - - // DeleteWorkspaceProviderByID deletes a workspace provider entity from the Coder control plane. - DeleteWorkspaceProviderByID(ctx context.Context, id string) error - - // Token returns the API Token used to authenticate. - Token() string - - // BaseURL returns the BaseURL configured for this Client. - BaseURL() url.URL - - // CordonWorkspaceProvider prevents the provider from having any more workspaces placed on it. - CordonWorkspaceProvider(ctx context.Context, id, reason string) error - - // UnCordonWorkspaceProvider changes an existing cordoned providers status to 'Ready'; - // allowing it to continue creating new workspaces and provisioning resources for them. - UnCordonWorkspaceProvider(ctx context.Context, id string) error - - // RenameWorkspaceProvider changes an existing providers name field. - RenameWorkspaceProvider(ctx context.Context, id string, name string) error - - // SetPolicyTemplate sets the workspace policy template - SetPolicyTemplate(ctx context.Context, templateID string, templateScope TemplateScope, dryRun bool) (*SetPolicyTemplateResponse, error) - - // Satellites fetches all satellitess known to the Coder control plane. - Satellites(ctx context.Context) ([]Satellite, error) - - // CreateSatellite creates a new satellite entity. - CreateSatellite(ctx context.Context, req CreateSatelliteReq) (*Satellite, error) - - // DeleteSatelliteByID deletes a satellite entity from the Coder control plane. - DeleteSatelliteByID(ctx context.Context, id string) error - - // UpdateLastConnectionAt updates the last connection at attribute of a workspace. - UpdateLastConnectionAt(ctx context.Context, workspaceID string) error - - // ICEServers fetches the list of ICE servers advertised by the deployment. - ICEServers(ctx context.Context) ([]webrtc.ICEServer, error) -} diff --git a/coder-sdk/login.go b/coder-sdk/login.go deleted file mode 100644 index 6899df12..00000000 --- a/coder-sdk/login.go +++ /dev/null @@ -1,69 +0,0 @@ -package coder - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "net/url" - "strings" - - "golang.org/x/xerrors" -) - -// LoginRequest is a request to authenticate using email -// and password credentials. -// -// This is provided for use in tests, and we recommend users authenticate -// using an API Token. -type LoginRequest struct { - Email string `json:"email"` - Password string `json:"password"` -} - -// LoginResponse contains successful response data for an authentication -// request, including an API Token to be used for subsequent requests. -// -// This is provided for use in tests, and we recommend users authenticate -// using an API Token. -type LoginResponse struct { - SessionToken string `json:"session_token"` -} - -// LoginWithPassword exchanges the email/password pair for -// a Session Token. -// -// If client is nil, the http.DefaultClient will be used. -func LoginWithPassword(ctx context.Context, client *http.Client, baseURL *url.URL, req *LoginRequest) (resp *LoginResponse, err error) { - if client == nil { - client = http.DefaultClient - } - - url := *baseURL - url.Path = fmt.Sprint(strings.TrimSuffix(url.Path, "/"), "/auth/basic/login") - - buf := &bytes.Buffer{} - err = json.NewEncoder(buf).Encode(req) - if err != nil { - return nil, xerrors.Errorf("failed to marshal JSON: %w", err) - } - - request, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), buf) - if err != nil { - return nil, xerrors.Errorf("failed to create request: %w", err) - } - - response, err := client.Do(request) - if err != nil { - return nil, xerrors.Errorf("error processing login request: %w", err) - } - defer response.Body.Close() - - err = json.NewDecoder(response.Body).Decode(&resp) - if err != nil { - return nil, xerrors.Errorf("failed to decode response: %w", err) - } - - return resp, nil -} diff --git a/coder-sdk/org.go b/coder-sdk/org.go deleted file mode 100644 index 0922d229..00000000 --- a/coder-sdk/org.go +++ /dev/null @@ -1,101 +0,0 @@ -package coder - -import ( - "context" - "net/http" - "time" -) - -// Organization describes an Organization in Coder. -type Organization struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Default bool `json:"default"` - Members []OrganizationUser `json:"members"` - WorkspaceCount int `json:"workspace_count"` - ResourceNamespace string `json:"resource_namespace"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - AutoOffThreshold Duration `json:"auto_off_threshold"` - CPUProvisioningRate float32 `json:"cpu_provisioning_rate"` - MemoryProvisioningRate float32 `json:"memory_provisioning_rate"` -} - -// OrganizationUser user wraps the basic User type and adds data specific to the user's membership of an organization. -type OrganizationUser struct { - User - OrganizationRoles []Role `json:"organization_roles"` - RolesUpdatedAt time.Time `json:"roles_updated_at"` -} - -// Organization Roles. -const ( - RoleOrgMember Role = "organization-member" - RoleOrgAdmin Role = "organization-admin" - RoleOrgManager Role = "organization-manager" -) - -// Organizations gets all Organizations. -func (c *DefaultClient) Organizations(ctx context.Context) ([]Organization, error) { - var orgs []Organization - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/orgs", nil, &orgs); err != nil { - return nil, err - } - return orgs, nil -} - -// OrganizationByID get the Organization by its ID. -func (c *DefaultClient) OrganizationByID(ctx context.Context, orgID string) (*Organization, error) { - var org Organization - err := c.requestBody(ctx, http.MethodGet, "/api/v0/orgs/"+orgID, nil, &org) - if err != nil { - return nil, err - } - return &org, nil -} - -// OrganizationMembers get all members of the given organization. -func (c *DefaultClient) OrganizationMembers(ctx context.Context, orgID string) ([]OrganizationUser, error) { - var members []OrganizationUser - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/orgs/"+orgID+"/members", nil, &members); err != nil { - return nil, err - } - return members, nil -} - -// UpdateOrganizationReq describes the patch request parameters to provide partial updates to an Organization resource. -type UpdateOrganizationReq struct { - Name *string `json:"name"` - Description *string `json:"description"` - Default *bool `json:"default"` - AutoOffThreshold *Duration `json:"auto_off_threshold"` - CPUProvisioningRate *float32 `json:"cpu_provisioning_rate"` - MemoryProvisioningRate *float32 `json:"memory_provisioning_rate"` -} - -// UpdateOrganization applys a partial update of an Organization resource. -func (c *DefaultClient) UpdateOrganization(ctx context.Context, orgID string, req UpdateOrganizationReq) error { - return c.requestBody(ctx, http.MethodPatch, "/api/v0/orgs/"+orgID, req, nil) -} - -// CreateOrganizationReq describes the request parameters to create a new Organization. -type CreateOrganizationReq struct { - Name string `json:"name"` - Description string `json:"description"` - Default bool `json:"default"` - ResourceNamespace string `json:"resource_namespace"` - AutoOffThreshold Duration `json:"auto_off_threshold"` - CPUProvisioningRate float32 `json:"cpu_provisioning_rate"` - MemoryProvisioningRate float32 `json:"memory_provisioning_rate"` -} - -// CreateOrganization creates a new Organization in Coder. -func (c *DefaultClient) CreateOrganization(ctx context.Context, req CreateOrganizationReq) error { - return c.requestBody(ctx, http.MethodPost, "/api/v0/orgs", req, nil) -} - -// DeleteOrganization deletes an organization. -func (c *DefaultClient) DeleteOrganization(ctx context.Context, orgID string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/v0/orgs/"+orgID, nil, nil) -} diff --git a/coder-sdk/registries.go b/coder-sdk/registries.go deleted file mode 100644 index 074155b3..00000000 --- a/coder-sdk/registries.go +++ /dev/null @@ -1,60 +0,0 @@ -package coder - -import ( - "context" - "net/http" - "net/url" - "time" -) - -// Registry defines an image registry configuration. -type Registry struct { - ID string `json:"id"` - OrganizationID string `json:"organization_id"` - FriendlyName string `json:"friendly_name"` - Registry string `json:"registry"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// Registries fetches all registries in an organization. -func (c *DefaultClient) Registries(ctx context.Context, orgID string) ([]Registry, error) { - var ( - r []Registry - query = url.Values{} - ) - - query.Set("org", orgID) - - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/registries", nil, &r, withQueryParams(query)); err != nil { - return nil, err - } - return r, nil -} - -// RegistryByID fetches a registry resource by its ID. -func (c *DefaultClient) RegistryByID(ctx context.Context, registryID string) (*Registry, error) { - var r Registry - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/registries/"+registryID, nil, &r); err != nil { - return nil, err - } - return &r, nil -} - -// UpdateRegistryReq defines the requests parameters for a partial update of a registry resource. -type UpdateRegistryReq struct { - Registry *string `json:"registry"` - FriendlyName *string `json:"friendly_name"` - Username *string `json:"username"` - Password *string `json:"password"` -} - -// UpdateRegistry applies a partial update to a registry resource. -func (c *DefaultClient) UpdateRegistry(ctx context.Context, registryID string, req UpdateRegistryReq) error { - return c.requestBody(ctx, http.MethodPatch, "/api/v0/registries/"+registryID, req, nil) -} - -// DeleteRegistry deletes a registry resource by its ID. -func (c *DefaultClient) DeleteRegistry(ctx context.Context, registryID string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/v0/registries/"+registryID, nil, nil) -} diff --git a/coder-sdk/request.go b/coder-sdk/request.go deleted file mode 100644 index d8f8bb76..00000000 --- a/coder-sdk/request.go +++ /dev/null @@ -1,127 +0,0 @@ -package coder - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "os" - "strings" - - "golang.org/x/xerrors" -) - -type requestOptions struct { - BaseURLOverride *url.URL - Query url.Values - Headers http.Header - Reader io.Reader -} - -type requestOption func(*requestOptions) - -// withQueryParams sets the provided query parameters on the request. -func withQueryParams(q url.Values) func(o *requestOptions) { - return func(o *requestOptions) { - o.Query = q - } -} - -func withHeaders(h http.Header) func(o *requestOptions) { - return func(o *requestOptions) { - o.Headers = h - } -} - -func withBaseURL(base *url.URL) func(o *requestOptions) { - return func(o *requestOptions) { - o.BaseURLOverride = base - } -} - -func withBody(w io.Reader) func(o *requestOptions) { - return func(o *requestOptions) { - o.Reader = w - } -} - -// request is a helper to set the cookie, marshal the payload and execute the request. -func (c *DefaultClient) request(ctx context.Context, method, path string, in interface{}, options ...requestOption) (*http.Response, error) { - url := *c.baseURL - - var config requestOptions - for _, o := range options { - o(&config) - } - if config.BaseURLOverride != nil { - url = *config.BaseURLOverride - } - if config.Query != nil { - url.RawQuery = config.Query.Encode() - } - url.Path = fmt.Sprint(strings.TrimSuffix(url.Path, "/"), path) - - // If we have incoming data, encode it as json. - var payload io.Reader - if in != nil { - body, err := json.Marshal(in) - if err != nil { - return nil, xerrors.Errorf("marshal request: %w", err) - } - payload = bytes.NewReader(body) - } - - if config.Reader != nil { - payload = config.Reader - } - - // Create the http request. - req, err := http.NewRequestWithContext(ctx, method, url.String(), payload) - if err != nil { - return nil, xerrors.Errorf("create request: %w", err) - } - - if config.Headers == nil { - req.Header = http.Header{} - } else { - req.Header = config.Headers - } - - // Provide the session token in a header - req.Header.Set("Session-Token", c.token) - - customAuthHeader, ok := os.LookupEnv("ENDPOINT_AUTH_HEADER") - if ok { - req.Header.Set("Authorization", customAuthHeader) - } - - // Execute the request. - return c.httpClient.Do(req) -} - -// requestBody is a helper extending the Client.request helper, checking the response code -// and decoding the response payload. -func (c *DefaultClient) requestBody(ctx context.Context, method, path string, in, out interface{}, opts ...requestOption) error { - resp, err := c.request(ctx, method, path, in, opts...) - if err != nil { - return xerrors.Errorf("Execute request: %q", err) - } - defer func() { _ = resp.Body.Close() }() // Best effort, likely connection dropped. - - // Responses in the 100 are handled by the http lib, in the 200 range, we have a success. - // Consider anything at or above 300 to be an error. - if resp.StatusCode > 299 { - return fmt.Errorf("unexpected status code %d: %w", resp.StatusCode, NewHTTPError(resp)) - } - - // If we expect a payload, process it as json. - if out != nil { - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return xerrors.Errorf("decode response body: %w", err) - } - } - return nil -} diff --git a/coder-sdk/satellite.go b/coder-sdk/satellite.go deleted file mode 100644 index 975ee32d..00000000 --- a/coder-sdk/satellite.go +++ /dev/null @@ -1,51 +0,0 @@ -package coder - -import ( - "context" - "net/http" -) - -type Satellite struct { - ID string `json:"id"` - Name string `json:"name"` - Fingerprint string `json:"fingerprint"` -} - -type satellites struct { - Data []Satellite `json:"data"` -} - -type createSatelliteResponse struct { - Data Satellite `json:"data"` -} - -// Satellites fetches all satellitess known to the Coder control plane. -func (c *DefaultClient) Satellites(ctx context.Context) ([]Satellite, error) { - var res satellites - err := c.requestBody(ctx, http.MethodGet, "/api/private/satellites", nil, &res) - if err != nil { - return nil, err - } - return res.Data, nil -} - -// CreateSatelliteReq defines the request parameters for creating a new satellite entity. -type CreateSatelliteReq struct { - Name string `json:"name"` - PublicKey string `json:"public_key"` -} - -// CreateSatellite creates a new satellite entity. -func (c *DefaultClient) CreateSatellite(ctx context.Context, req CreateSatelliteReq) (*Satellite, error) { - var res createSatelliteResponse - err := c.requestBody(ctx, http.MethodPost, "/api/private/satellites", req, &res) - if err != nil { - return nil, err - } - return &res.Data, nil -} - -// DeleteSatelliteByID deletes a satellite entity from the Coder control plane. -func (c *DefaultClient) DeleteSatelliteByID(ctx context.Context, id string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/private/satellites/"+id, nil, nil) -} diff --git a/coder-sdk/tags.go b/coder-sdk/tags.go deleted file mode 100644 index 9a3c941f..00000000 --- a/coder-sdk/tags.go +++ /dev/null @@ -1,72 +0,0 @@ -package coder - -import ( - "context" - "net/http" - "time" -) - -// ImageTag is a Docker image tag. -type ImageTag struct { - ImageID string `json:"image_id" table:"-"` - Tag string `json:"tag" table:"Tag"` - LatestHash string `json:"latest_hash" table:"-"` - HashLastUpdatedAt time.Time `json:"hash_last_updated_at" table:"-"` - OSRelease *OSRelease `json:"os_release" table:"OS"` - Workspaces []*Workspace `json:"workspaces" table:"-"` - UpdatedAt time.Time `json:"updated_at" table:"UpdatedAt"` - CreatedAt time.Time `json:"created_at" table:"-"` -} - -func (i ImageTag) String() string { - return i.Tag -} - -// OSRelease is the marshalled /etc/os-release file. -type OSRelease struct { - ID string `json:"id"` - PrettyName string `json:"pretty_name"` - HomeURL string `json:"home_url"` -} - -func (o OSRelease) String() string { - return o.PrettyName -} - -// CreateImageTagReq defines the request parameters for creating a new image tag. -type CreateImageTagReq struct { - Tag string `json:"tag"` - Default bool `json:"default"` -} - -// CreateImageTag creates a new image tag resource. -func (c *DefaultClient) CreateImageTag(ctx context.Context, imageID string, req CreateImageTagReq) (*ImageTag, error) { - var tag ImageTag - if err := c.requestBody(ctx, http.MethodPost, "/api/v0/images/"+imageID+"/tags", req, tag); err != nil { - return nil, err - } - return &tag, nil -} - -// DeleteImageTag deletes an image tag resource. -func (c *DefaultClient) DeleteImageTag(ctx context.Context, imageID, tag string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/v0/images/"+imageID+"/tags/"+tag, nil, nil) -} - -// ImageTags fetch all image tags. -func (c *DefaultClient) ImageTags(ctx context.Context, imageID string) ([]ImageTag, error) { - var tags []ImageTag - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/images/"+imageID+"/tags", nil, &tags); err != nil { - return nil, err - } - return tags, nil -} - -// ImageTagByID fetch an image tag by ID. -func (c *DefaultClient) ImageTagByID(ctx context.Context, imageID, tagID string) (*ImageTag, error) { - var tag ImageTag - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/images/"+imageID+"/tags/"+tagID, nil, &tag); err != nil { - return nil, err - } - return &tag, nil -} diff --git a/coder-sdk/tokens.go b/coder-sdk/tokens.go deleted file mode 100644 index dc12a173..00000000 --- a/coder-sdk/tokens.go +++ /dev/null @@ -1,67 +0,0 @@ -package coder - -import ( - "context" - "net/http" - "time" -) - -// APIToken describes a Coder APIToken resource for use in API requests. -type APIToken struct { - ID string `json:"id"` - Name string `json:"name"` - Application bool `json:"application"` - UserID string `json:"user_id"` - LastUsed time.Time `json:"last_used"` -} - -// CreateAPITokenReq defines the paramemters for creating a new APIToken. -type CreateAPITokenReq struct { - Name string `json:"name"` -} - -type createAPITokenResp struct { - Key string `json:"key"` -} - -// CreateAPIToken creates a new APIToken for making authenticated requests to Coder. -func (c *DefaultClient) CreateAPIToken(ctx context.Context, userID string, req CreateAPITokenReq) (token string, _ error) { - var resp createAPITokenResp - err := c.requestBody(ctx, http.MethodPost, "/api/v0/api-keys/"+userID, req, &resp) - if err != nil { - return "", err - } - return resp.Key, nil -} - -// APITokens fetches all APITokens owned by the given user. -func (c *DefaultClient) APITokens(ctx context.Context, userID string) ([]APIToken, error) { - var tokens []APIToken - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/api-keys/"+userID, nil, &tokens); err != nil { - return nil, err - } - return tokens, nil -} - -// APITokenByID fetches the metadata for a given APIToken. -func (c *DefaultClient) APITokenByID(ctx context.Context, userID, tokenID string) (*APIToken, error) { - var token APIToken - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/api-keys/"+userID+"/"+tokenID, nil, &token); err != nil { - return nil, err - } - return &token, nil -} - -// DeleteAPIToken deletes an APIToken. -func (c *DefaultClient) DeleteAPIToken(ctx context.Context, userID, tokenID string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/v0/api-keys/"+userID+"/"+tokenID, nil, nil) -} - -// RegenerateAPIToken regenerates the given APIToken and returns the new value. -func (c *DefaultClient) RegenerateAPIToken(ctx context.Context, userID, tokenID string) (token string, _ error) { - var resp createAPITokenResp - if err := c.requestBody(ctx, http.MethodPost, "/api/v0/api-keys/"+userID+"/"+tokenID+"/regen", nil, &resp); err != nil { - return "", err - } - return resp.Key, nil -} diff --git a/coder-sdk/users.go b/coder-sdk/users.go deleted file mode 100644 index 8b02f5f1..00000000 --- a/coder-sdk/users.go +++ /dev/null @@ -1,165 +0,0 @@ -package coder - -import ( - "context" - "net/http" - "time" -) - -// User describes a Coder user account. -type User struct { - ID string `json:"id" table:"-"` - Email string `json:"email" table:"Email"` - Username string `json:"username" table:"Username"` - Name string `json:"name" table:"Name"` - Roles []Role `json:"roles" table:"-"` - TemporaryPassword bool `json:"temporary_password" table:"-"` - LoginType string `json:"login_type" table:"-"` - KeyRegeneratedAt time.Time `json:"key_regenerated_at" table:"-"` - CreatedAt time.Time `json:"created_at" table:"CreatedAt"` - UpdatedAt time.Time `json:"updated_at" table:"-"` -} - -// Role defines a Coder permissions role group. -type Role string - -// Site Roles. -const ( - SiteAdmin Role = "site-admin" - SiteAuditor Role = "site-auditor" - SiteManager Role = "site-manager" - SiteMember Role = "site-member" -) - -// LoginType defines the enum of valid user login types. -type LoginType string - -// LoginType enum options. -const ( - LoginTypeBuiltIn LoginType = "built-in" - LoginTypeSAML LoginType = "saml" - LoginTypeOIDC LoginType = "oidc" -) - -// Me gets the details of the authenticated user. -func (c *DefaultClient) Me(ctx context.Context) (*User, error) { - return c.UserByID(ctx, Me) -} - -// UserByID get the details of a user by their id. -func (c *DefaultClient) UserByID(ctx context.Context, id string) (*User, error) { - var u User - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/users/"+id, nil, &u); err != nil { - return nil, err - } - return &u, nil -} - -// SSHKey describes an SSH keypair. -type SSHKey struct { - PublicKey string `json:"public_key"` - PrivateKey string `json:"private_key"` -} - -// SSHKey gets the current SSH kepair of the authenticated user. -func (c *DefaultClient) SSHKey(ctx context.Context) (*SSHKey, error) { - var key SSHKey - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/users/me/sshkey", nil, &key); err != nil { - return nil, err - } - return &key, nil -} - -// Users gets the list of user accounts. -func (c *DefaultClient) Users(ctx context.Context) ([]User, error) { - var u []User - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/users", nil, &u); err != nil { - return nil, err - } - return u, nil -} - -// UserByEmail gets a user by email. -func (c *DefaultClient) UserByEmail(ctx context.Context, email string) (*User, error) { - if email == Me { - return c.Me(ctx) - } - users, err := c.Users(ctx) - if err != nil { - return nil, err - } - for _, u := range users { - if u.Email == email { - return &u, nil - } - } - return nil, ErrNotFound -} - -// UpdateUserReq defines a modification to the user, updating the -// value of all non-nil values. -type UpdateUserReq struct { - *UserPasswordSettings - Revoked *bool `json:"revoked,omitempty"` - Roles *[]Role `json:"roles,omitempty"` - LoginType *LoginType `json:"login_type,omitempty"` - Name *string `json:"name,omitempty"` - Username *string `json:"username,omitempty"` - Email *string `json:"email,omitempty"` - DotfilesGitURL *string `json:"dotfiles_git_uri,omitempty"` -} - -// UserPasswordSettings allows modification of the user's password -// settings. -// -// These settings are only applicable to users managed using the -// built-in authentication provider; users authenticating using -// OAuth must change their password through the identity provider -// instead. -type UserPasswordSettings struct { - // OldPassword is the account's current password. - OldPassword string `json:"old_password,omitempty"` - - // Password is the new password, which may be a temporary password. - Password string `json:"password,omitempty"` - - // Temporary indicates that API access should be restricted to the - // password change API and a few other APIs. If set to true, Coder - // will prompt the user to change their password upon their next - // login through the web interface. - Temporary bool `json:"temporary_password,omitempty"` -} - -// UpdateUser applyes the partial update to the given user. -func (c *DefaultClient) UpdateUser(ctx context.Context, userID string, req UpdateUserReq) error { - return c.requestBody(ctx, http.MethodPatch, "/api/v0/users/"+userID, req, nil) -} - -// UpdateUXState applies a partial update of the user's UX State. -func (c *DefaultClient) UpdateUXState(ctx context.Context, userID string, uxsPartial map[string]interface{}) error { - if err := c.requestBody(ctx, http.MethodPut, "/api/private/users/"+userID+"/ux-state", uxsPartial, nil); err != nil { - return err - } - return nil -} - -// CreateUserReq defines the request parameters for creating a new user resource. -type CreateUserReq struct { - Name string `json:"name"` - Username string `json:"username"` - Email string `json:"email"` - Password string `json:"password"` - TemporaryPassword bool `json:"temporary_password"` - LoginType LoginType `json:"login_type"` - OrganizationsIDs []string `json:"organizations"` -} - -// CreateUser creates a new user account. -func (c *DefaultClient) CreateUser(ctx context.Context, req CreateUserReq) error { - return c.requestBody(ctx, http.MethodPost, "/api/v0/users", req, nil) -} - -// DeleteUser deletes a user account. -func (c *DefaultClient) DeleteUser(ctx context.Context, userID string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/v0/users/"+userID, nil, nil) -} diff --git a/coder-sdk/users_test.go b/coder-sdk/users_test.go deleted file mode 100644 index 31240880..00000000 --- a/coder-sdk/users_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package coder_test - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/coder-sdk" -) - -func TestUsers(t *testing.T) { - t.Parallel() - - const username = "root" - const name = "Charlie Root" - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "Users is a GET", http.MethodGet, r.Method) - assert.Equal(t, "Path matches", "/api/v0/users", r.URL.Path) - - users := []map[string]interface{}{ - { - "id": "default", - "email": "root@user.com", - "username": username, - "name": name, - "roles": []coder.Role{coder.SiteAdmin}, - "temporary_password": false, - "login_type": coder.LoginTypeBuiltIn, - "key_regenerated_at": time.Now(), - "created_at": time.Now(), - "updated_at": time.Now(), - }, - } - - w.WriteHeader(http.StatusOK) - err := json.NewEncoder(w).Encode(users) - assert.Success(t, "error encoding JSON", err) - })) - t.Cleanup(func() { - server.Close() - }) - - u, err := url.Parse(server.URL) - assert.Success(t, "failed to parse test server URL", err) - - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - Token: "JcmErkJjju-KSrztst0IJX7xGJhKQPtfv", - }) - assert.Success(t, "failed to create coder.Client", err) - - users, err := client.Users(context.Background()) - assert.Success(t, "error getting Users", err) - assert.True(t, "users should return a single user", len(users) == 1) - assert.Equal(t, "expected user's name to match", name, users[0].Name) - assert.Equal(t, "expected user's username to match", username, users[0].Username) -} - -func TestUserUpdatePassword(t *testing.T) { - t.Parallel() - - const oldPassword = "vt9g9rxsptrq" - const newPassword = "wmf39jw2f7pk" - - server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "Users is a PATCH", http.MethodPatch, r.Method) - assert.Equal(t, "Path matches", "/api/v0/users/me", r.URL.Path) - - expected := map[string]interface{}{ - "old_password": oldPassword, - "password": newPassword, - } - var request map[string]interface{} - err := json.NewDecoder(r.Body).Decode(&request) - assert.Success(t, "error decoding JSON", err) - assert.Equal(t, "unexpected request data", expected, request) - - w.WriteHeader(http.StatusOK) - })) - t.Cleanup(func() { - server.Close() - }) - - u, err := url.Parse(server.URL) - assert.Success(t, "failed to parse test server URL", err) - - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - HTTPClient: server.Client(), - Token: "JcmErkJjju-KSrztst0IJX7xGJhKQPtfv", - }) - assert.Success(t, "failed to create coder.Client", err) - - err = client.UpdateUser(context.Background(), "me", coder.UpdateUserReq{ - UserPasswordSettings: &coder.UserPasswordSettings{ - OldPassword: oldPassword, - Password: newPassword, - Temporary: false, - }, - }) - assert.Success(t, "error when updating password", err) -} diff --git a/coder-sdk/util.go b/coder-sdk/util.go deleted file mode 100644 index 0abba3c9..00000000 --- a/coder-sdk/util.go +++ /dev/null @@ -1,36 +0,0 @@ -package coder - -import ( - "encoding/json" - "strconv" - "time" -) - -// String gives a string pointer. -func String(s string) *string { - return &s -} - -// Duration is a time.Duration wrapper that marshals to millisecond precision. -// While it looses precision, most javascript applications expect durations to be in milliseconds. -type Duration time.Duration - -// MarshalJSON marshals the duration to millisecond precision. -func (d Duration) MarshalJSON() ([]byte, error) { - du := time.Duration(d) - return json.Marshal(du.Milliseconds()) -} - -// UnmarshalJSON unmarshals a millisecond-precision integer to -// a time.Duration. -func (d *Duration) UnmarshalJSON(b []byte) error { - i, err := strconv.ParseInt(string(b), 10, 64) - if err != nil { - return err - } - - *d = Duration(time.Duration(i) * time.Millisecond) - return nil -} - -func (d Duration) String() string { return time.Duration(d).String() } diff --git a/coder-sdk/version.go b/coder-sdk/version.go deleted file mode 100644 index dd1ae9ec..00000000 --- a/coder-sdk/version.go +++ /dev/null @@ -1,23 +0,0 @@ -package coder - -import ( - "context" - "net/http" -) - -// APIVersion parses the coder-version http header from an authenticated request. -func (c *DefaultClient) APIVersion(ctx context.Context) (string, error) { - const coderVersionHeaderKey = "coder-version" - resp, err := c.request(ctx, http.MethodGet, "/api", nil) - if err != nil { - return "", err - } - defer resp.Body.Close() - - version := resp.Header.Get(coderVersionHeaderKey) - if version == "" { - version = "unknown" - } - - return version, nil -} diff --git a/coder-sdk/webrtc.go b/coder-sdk/webrtc.go deleted file mode 100644 index 4ea1713c..00000000 --- a/coder-sdk/webrtc.go +++ /dev/null @@ -1,23 +0,0 @@ -package coder - -import ( - "context" - "net/http" - - "github.com/pion/webrtc/v3" -) - -type getICEServersRes struct { - Data []webrtc.ICEServer `json:"data"` -} - -// ICEServers fetches the list of ICE servers advertised by the deployment. -func (c *DefaultClient) ICEServers(ctx context.Context) ([]webrtc.ICEServer, error) { - var res getICEServersRes - err := c.requestBody(ctx, http.MethodGet, "/api/private/webrtc/ice", nil, &res) - if err != nil { - return nil, err - } - - return res.Data, nil -} diff --git a/coder-sdk/workspace.go b/coder-sdk/workspace.go deleted file mode 100644 index e30c5c58..00000000 --- a/coder-sdk/workspace.go +++ /dev/null @@ -1,515 +0,0 @@ -package coder - -import ( - "context" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - "cdr.dev/wsep" - "golang.org/x/xerrors" - "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" -) - -// Workspace describes a Coder workspace. -type Workspace struct { - ID string `json:"id" table:"-"` - Name string `json:"name" table:"Name"` - ImageID string `json:"image_id" table:"-"` - ImageTag string `json:"image_tag" table:"ImageTag"` - OrganizationID string `json:"organization_id" table:"-"` - UserID string `json:"user_id" table:"-"` - LastBuiltAt time.Time `json:"last_built_at" table:"-"` - CPUCores float32 `json:"cpu_cores" table:"CPUCores"` - MemoryGB float32 `json:"memory_gb" table:"MemoryGB"` - DiskGB int `json:"disk_gb" table:"DiskGB"` - GPUs int `json:"gpus" table:"-"` - Updating bool `json:"updating" table:"-"` - LatestStat WorkspaceStat `json:"latest_stat" table:"Status"` - RebuildMessages []RebuildMessage `json:"rebuild_messages" table:"-"` - CreatedAt time.Time `json:"created_at" table:"-"` - UpdatedAt time.Time `json:"updated_at" table:"-"` - LastOpenedAt time.Time `json:"last_opened_at" table:"-"` - LastConnectionAt time.Time `json:"last_connection_at" table:"-"` - AutoOffThreshold Duration `json:"auto_off_threshold" table:"-"` - UseContainerVM bool `json:"use_container_vm" table:"CVM"` - ResourcePoolID string `json:"resource_pool_id" table:"-"` -} - -// RebuildMessage defines the message shown when a Workspace requires a rebuild for it can be accessed. -type RebuildMessage struct { - Text string `json:"text"` - Required bool `json:"required"` - AutoOffThreshold Duration `json:"auto_off_threshold"` -} - -// WorkspaceStat represents the state of a workspace. -type WorkspaceStat struct { - Time time.Time `json:"time"` - LastOnline time.Time `json:"last_online"` - ContainerStatus WorkspaceStatus `json:"container_status"` - StatError string `json:"stat_error"` - CPUUsage float32 `json:"cpu_usage"` - MemoryTotal int64 `json:"memory_total"` - MemoryUsage float32 `json:"memory_usage"` - DiskTotal int64 `json:"disk_total"` - DiskUsed int64 `json:"disk_used"` -} - -func (e WorkspaceStat) String() string { return string(e.ContainerStatus) } - -// WorkspaceStatus refers to the states of a workspace. -type WorkspaceStatus string - -// The following represent the possible workspace container states. -const ( - WorkspaceCreating WorkspaceStatus = "CREATING" - WorkspaceOff WorkspaceStatus = "OFF" - WorkspaceOn WorkspaceStatus = "ON" - WorkspaceFailed WorkspaceStatus = "FAILED" - WorkspaceUnknown WorkspaceStatus = "UNKNOWN" -) - -// CreateWorkspaceRequest is used to configure a new workspace. -type CreateWorkspaceRequest struct { - Name string `json:"name"` - ImageID string `json:"image_id"` - OrgID string `json:"org_id"` - ImageTag string `json:"image_tag"` - CPUCores float32 `json:"cpu_cores"` - MemoryGB float32 `json:"memory_gb"` - DiskGB int `json:"disk_gb"` - GPUs int `json:"gpus"` - UseContainerVM bool `json:"use_container_vm"` - ResourcePoolID string `json:"resource_pool_id"` - Namespace string `json:"namespace"` - EnableAutoStart bool `json:"autostart_enabled"` - - // ForUserID is an optional param to create a workspace for another user - // other than the requester. This only works for admins and site managers. - ForUserID string `json:"for_user_id,omitempty"` - - // TemplateID comes from the parse template route on cemanager. - TemplateID string `json:"template_id,omitempty"` -} - -// CreateWorkspace sends a request to create a workspace. -func (c *DefaultClient) CreateWorkspace(ctx context.Context, req CreateWorkspaceRequest) (*Workspace, error) { - var workspace Workspace - if err := c.requestBody(ctx, http.MethodPost, "/api/v0/workspaces", req, &workspace); err != nil { - return nil, err - } - return &workspace, nil -} - -// ParseTemplateRequest parses a template. If Local is a non-nil reader -// it will obviate any other fields on the request. -type ParseTemplateRequest struct { - RepoURL string `json:"repo_url"` - Ref string `json:"ref"` - Filepath string `json:"filepath"` - OrgID string `json:"-"` - Local io.Reader `json:"-"` -} - -// TemplateVersion is a workspace template. -// For now, let's not interpret it on the CLI level. We just need -// to forward this as part of the create workspace request. -type TemplateVersion struct { - ID string `json:"id"` - TemplateID string `json:"template_id"` - // FileHash is the sha256 hash of the template's file contents. - FileHash string `json:"file_hash"` - // Commit is the git commit from which the template was derived. - Commit string `json:"commit"` - CommitMessage string `json:"commit_message"` - CreatedAt time.Time `json:"created_at"` -} - -// ParseTemplate parses a template config. It support both remote repositories and local files. -// If a local file is specified then all other values in the request are ignored. -func (c *DefaultClient) ParseTemplate(ctx context.Context, req ParseTemplateRequest) (*TemplateVersion, error) { - const path = "/api/private/workspaces/template/parse" - var ( - tpl TemplateVersion - opts []requestOption - headers = http.Header{} - query = url.Values{} - ) - - query.Set("org-id", req.OrgID) - - opts = append(opts, withQueryParams(query)) - - if req.Local == nil { - if err := c.requestBody(ctx, http.MethodPost, path, req, &tpl, opts...); err != nil { - return &tpl, err - } - return &tpl, nil - } - - headers.Set("Content-Type", "application/octet-stream") - opts = append(opts, withBody(req.Local), withHeaders(headers)) - - err := c.requestBody(ctx, http.MethodPost, path, nil, &tpl, opts...) - if err != nil { - return &tpl, err - } - - return &tpl, nil -} - -// CreateWorkspaceFromRepo sends a request to create a workspace from a repository. -func (c *DefaultClient) CreateWorkspaceFromRepo(ctx context.Context, orgID string, req TemplateVersion) (*Workspace, error) { - var workspace Workspace - if err := c.requestBody(ctx, http.MethodPost, "/api/private/orgs/"+orgID+"/workspaces/from-repo", req, &workspace); err != nil { - return nil, err - } - return &workspace, nil -} - -// Workspaces lists workspaces returned by the given filter. -// TODO: add the filter options, explore performance issue. -func (c *DefaultClient) Workspaces(ctx context.Context) ([]Workspace, error) { - var workspaces []Workspace - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/workspaces", nil, &workspaces); err != nil { - return nil, err - } - return workspaces, nil -} - -// UserWorkspacesByOrganization gets the list of workspaces owned by the given user. -func (c *DefaultClient) UserWorkspacesByOrganization(ctx context.Context, userID, orgID string) ([]Workspace, error) { - var ( - workspaces []Workspace - query = url.Values{} - ) - - query.Add("orgs", orgID) - query.Add("users", userID) - - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/workspaces", nil, &workspaces, withQueryParams(query)); err != nil { - return nil, err - } - return workspaces, nil -} - -// DeleteWorkspace deletes the workspace. -func (c *DefaultClient) DeleteWorkspace(ctx context.Context, workspaceID string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/v0/workspaces/"+workspaceID, nil, nil) -} - -// StopWorkspace stops the workspace. -func (c *DefaultClient) StopWorkspace(ctx context.Context, workspaceID string) error { - return c.requestBody(ctx, http.MethodPut, "/api/v0/workspaces/"+workspaceID+"/stop", nil, nil) -} - -// UpdateWorkspaceReq defines the update operation, only setting -// nil-fields. -type UpdateWorkspaceReq struct { - ImageID *string `json:"image_id"` - ImageTag *string `json:"image_tag"` - CPUCores *float32 `json:"cpu_cores"` - MemoryGB *float32 `json:"memory_gb"` - DiskGB *int `json:"disk_gb"` - GPUs *int `json:"gpus"` - TemplateID *string `json:"template_id"` -} - -// RebuildWorkspace requests that the given workspaceID is rebuilt with no changes to its specification. -func (c *DefaultClient) RebuildWorkspace(ctx context.Context, workspaceID string) error { - return c.requestBody(ctx, http.MethodPatch, "/api/v0/workspaces/"+workspaceID, UpdateWorkspaceReq{}, nil) -} - -// EditWorkspace modifies the workspace specification and initiates a rebuild. -func (c *DefaultClient) EditWorkspace(ctx context.Context, workspaceID string, req UpdateWorkspaceReq) error { - return c.requestBody(ctx, http.MethodPatch, "/api/v0/workspaces/"+workspaceID, req, nil) -} - -// DialWsep dials a workspace's command execution interface -// See https://github.com/cdr/wsep for details. -func (c *DefaultClient) DialWsep(ctx context.Context, baseURL *url.URL, workspaceID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/proxy/workspaces/"+workspaceID+"/wsep", withBaseURL(baseURL)) -} - -// DialExecutor gives a remote execution interface for performing commands -// inside a workspace. -func (c *DefaultClient) DialExecutor(ctx context.Context, baseURL *url.URL, workspaceID string) (wsep.Execer, error) { - ws, err := c.DialWsep(ctx, baseURL, workspaceID) - if err != nil { - return nil, err - } - return wsep.RemoteExecer(ws), nil -} - -// DialIDEStatus opens a websocket connection for cpu load metrics on the workspace. -func (c *DefaultClient) DialIDEStatus(ctx context.Context, baseURL *url.URL, workspaceID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/proxy/workspaces/"+workspaceID+"/ide/api/status", withBaseURL(baseURL)) -} - -// DialWorkspaceBuildLog opens a websocket connection for the workspace build log messages. -func (c *DefaultClient) DialWorkspaceBuildLog(ctx context.Context, workspaceID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/api/private/workspaces/"+workspaceID+"/watch-update") -} - -// BuildLog defines a build log record for a Coder workspace. -type BuildLog struct { - ID string `db:"id" json:"id"` - WorkspaceID string `db:"workspace_id" json:"workspace_id"` - // BuildID allows the frontend to separate the logs from the old build with the logs from the new. - BuildID string `db:"build_id" json:"build_id"` - Time time.Time `db:"time" json:"time"` - Type BuildLogType `db:"type" json:"type"` - Msg string `db:"msg" json:"msg"` -} - -// BuildLogFollowMsg wraps the base BuildLog and adds a field for collecting -// errors that may occur when follow or parsing. -type BuildLogFollowMsg struct { - BuildLog - Err error -} - -// FollowWorkspaceBuildLog trails the build log of a Coder workspace. -func (c *DefaultClient) FollowWorkspaceBuildLog(ctx context.Context, workspaceID string) (<-chan BuildLogFollowMsg, error) { - ch := make(chan BuildLogFollowMsg) - ws, err := c.DialWorkspaceBuildLog(ctx, workspaceID) - if err != nil { - return nil, err - } - go func() { - defer ws.Close(websocket.StatusNormalClosure, "normal closure") - defer close(ch) - for { - var msg BuildLog - if err := wsjson.Read(ctx, ws, &msg); err != nil { - ch <- BuildLogFollowMsg{Err: err} - if xerrors.Is(err, context.Canceled) || xerrors.Is(err, context.DeadlineExceeded) { - return - } - continue - } - ch <- BuildLogFollowMsg{BuildLog: msg} - } - }() - return ch, nil -} - -// DialWorkspaceStats opens a websocket connection for workspace stats. -func (c *DefaultClient) DialWorkspaceStats(ctx context.Context, workspaceID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/api/private/workspaces/"+workspaceID+"/watch-stats") -} - -// DialResourceLoad opens a websocket connection for cpu load metrics on the workspace. -func (c *DefaultClient) DialResourceLoad(ctx context.Context, workspaceID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/api/private/workspaces/"+workspaceID+"/watch-resource-load") -} - -// BuildLogType describes the type of an event. -type BuildLogType string - -const ( - // BuildLogTypeStart signals that a new build log has begun. - BuildLogTypeStart BuildLogType = "start" - // BuildLogTypeStage is a stage-level event for a workspace. - // It can be thought of as a major step in the workspace's - // lifecycle. - BuildLogTypeStage BuildLogType = "stage" - // BuildLogTypeError describes an error that has occurred. - BuildLogTypeError BuildLogType = "error" - // BuildLogTypeSubstage describes a subevent that occurs as - // part of a stage. This can be the output from a user's - // personalization script, or a long running command. - BuildLogTypeSubstage BuildLogType = "substage" - // BuildLogTypeDone signals that the build has completed. - BuildLogTypeDone BuildLogType = "done" -) - -type buildLogMsg struct { - Type BuildLogType `json:"type"` -} - -// WaitForWorkspaceReady will watch the build log and return when done. -func (c *DefaultClient) WaitForWorkspaceReady(ctx context.Context, workspaceID string) error { - conn, err := c.DialWorkspaceBuildLog(ctx, workspaceID) - if err != nil { - return xerrors.Errorf("%s: dial build log: %w", workspaceID, err) - } - - for { - msg := buildLogMsg{} - err := wsjson.Read(ctx, conn, &msg) - if err != nil { - return xerrors.Errorf("%s: reading build log msg: %w", workspaceID, err) - } - - if msg.Type == BuildLogTypeDone { - return nil - } - } -} - -// WorkspaceByID get the details of a workspace by its id. -func (c *DefaultClient) WorkspaceByID(ctx context.Context, id string) (*Workspace, error) { - var workspace Workspace - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/workspaces/"+id, nil, &workspace); err != nil { - return nil, err - } - return &workspace, nil -} - -// WorkspacesByWorkspaceProvider returns all workspaces that belong to a particular workspace provider. -func (c *DefaultClient) WorkspacesByWorkspaceProvider(ctx context.Context, wpID string) ([]Workspace, error) { - var workspaces []Workspace - if err := c.requestBody(ctx, http.MethodGet, "/api/private/resource-pools/"+wpID+"/workspaces", nil, &workspaces); err != nil { - return nil, err - } - return workspaces, nil -} - -const ( - // SkipTemplateOrg allows skipping checks on organizations. - SkipTemplateOrg = "SKIP_ORG" -) - -type TemplateScope string - -const ( - // TemplateScopeSite is the scope for a site wide policy template. - TemplateScopeSite = "site" -) - -type SetPolicyTemplateRequest struct { - TemplateID string `json:"template_id"` - Type string `json:"type"` // site, org -} - -type SetPolicyTemplateResponse struct { - MergeConflicts []*WorkspaceTemplateMergeConflict `json:"merge_conflicts"` -} - -type WorkspaceTemplateMergeConflict struct { - WorkspaceID string `json:"workspace_id"` - CurrentTemplateWarnings []string `json:"current_template_warnings"` - CurrentTemplateError *TplError `json:"current_template_errors"` - LatestTemplateWarnings []string `json:"latest_template_warnings"` - LatestTemplateError *TplError `json:"latest_template_errors"` - CurrentTemplateIsLatest bool `json:"current_template_is_latest"` - Message string `json:"message"` -} - -func (mc WorkspaceTemplateMergeConflict) String() string { - var sb strings.Builder - - if mc.Message != "" { - sb.WriteString(mc.Message) - } - - currentConflicts := len(mc.CurrentTemplateWarnings) != 0 || mc.CurrentTemplateError != nil - updateConflicts := len(mc.LatestTemplateWarnings) != 0 || mc.LatestTemplateError != nil - - if !currentConflicts && !updateConflicts { - sb.WriteString("No workspace conflicts\n") - return sb.String() - } - - if currentConflicts { - if len(mc.CurrentTemplateWarnings) != 0 { - fmt.Fprintf(&sb, "Warnings: \n%s\n", strings.Join(mc.CurrentTemplateWarnings, "\n")) - } - if mc.CurrentTemplateError != nil { - fmt.Fprintf(&sb, "Errors: \n%s\n", strings.Join(mc.CurrentTemplateError.Msgs, "\n")) - } - } - - if !mc.CurrentTemplateIsLatest && updateConflicts { - sb.WriteString("If workspace is updated to the latest template:\n") - if len(mc.LatestTemplateWarnings) != 0 { - fmt.Fprintf(&sb, "Warnings: \n%s\n", strings.Join(mc.LatestTemplateWarnings, "\n")) - } - if mc.LatestTemplateError != nil { - fmt.Fprintf(&sb, "Errors: \n%s\n", strings.Join(mc.LatestTemplateError.Msgs, "\n")) - } - } - - return sb.String() -} - -type WorkspaceTemplateMergeConflicts []*WorkspaceTemplateMergeConflict - -func (mcs WorkspaceTemplateMergeConflicts) Summary() string { - var ( - sb strings.Builder - currentWarnings int - updateWarnings int - currentErrors int - updateErrors int - ) - - for _, mc := range mcs { - if len(mc.CurrentTemplateWarnings) != 0 { - currentWarnings++ - } - if len(mc.LatestTemplateWarnings) != 0 { - updateWarnings++ - } - if mc.CurrentTemplateError != nil { - currentErrors++ - } - if mc.LatestTemplateError != nil { - updateErrors++ - } - } - - if currentErrors == 0 && updateErrors == 0 && currentWarnings == 0 && updateWarnings == 0 { - sb.WriteString("No workspace conflicts\n") - return sb.String() - } - - if currentErrors != 0 { - fmt.Fprintf(&sb, "%d workspaces will not be able to be rebuilt\n", currentErrors) - } - if updateErrors != 0 { - fmt.Fprintf(&sb, "%d workspaces will not be able to be rebuilt if updated to the latest version\n", updateErrors) - } - if currentWarnings != 0 { - fmt.Fprintf(&sb, "%d workspaces will be impacted\n", currentWarnings) - } - if updateWarnings != 0 { - fmt.Fprintf(&sb, "%d workspaces will be impacted if updated to the latest version\n", updateWarnings) - } - - return sb.String() -} - -type TplError struct { - // Msgs are the human facing strings to present to the user. Since there can be multiple - // problems with a template, there might be multiple strings - Msgs []string `json:"messages"` -} - -func (c *DefaultClient) SetPolicyTemplate(ctx context.Context, templateID string, templateScope TemplateScope, dryRun bool) (*SetPolicyTemplateResponse, error) { - var ( - resp SetPolicyTemplateResponse - query = url.Values{} - ) - - req := SetPolicyTemplateRequest{ - TemplateID: templateID, - Type: string(templateScope), - } - - if dryRun { - query.Set("dry-run", "true") - } - - if err := c.requestBody(ctx, http.MethodPost, "/api/private/workspaces/template/policy", req, &resp, withQueryParams(query)); err != nil { - return nil, err - } - - return &resp, nil -} diff --git a/coder-sdk/workspace_providers.go b/coder-sdk/workspace_providers.go deleted file mode 100644 index 1ed0589c..00000000 --- a/coder-sdk/workspace_providers.go +++ /dev/null @@ -1,140 +0,0 @@ -package coder - -import ( - "context" - "net/http" -) - -// WorkspaceProviders defines all available Coder workspace provider targets. -type WorkspaceProviders struct { - Kubernetes []KubernetesProvider `json:"kubernetes"` -} - -// KubernetesProvider defines an entity capable of deploying and acting as an ingress for Coder workspaces. -type KubernetesProvider struct { - ID string `json:"id" table:"-"` - Name string `json:"name" table:"Name"` - Status WorkspaceProviderStatus `json:"status" table:"Status"` - BuiltIn bool `json:"built_in" table:"-"` - EnvproxyAccessURL string `json:"envproxy_access_url" table:"Access URL" validate:"required"` - DevurlHost string `json:"devurl_host" table:"Devurl Host"` - OrgWhitelist []string `json:"org_whitelist" table:"-"` - KubeProviderConfig `json:"config" table:"_"` -} - -// KubeProviderConfig defines Kubernetes-specific configuration options. -type KubeProviderConfig struct { - ClusterAddress string `json:"cluster_address" table:"Cluster Address"` - DefaultNamespace string `json:"default_namespace" table:"Namespace"` - StorageClass string `json:"storage_class" table:"Storage Class"` - ClusterDomainSuffix string `json:"cluster_domain_suffix" table:"Cluster Domain Suffix"` - SSHEnabled bool `json:"ssh_enabled" table:"SSH Enabled"` -} - -// WorkspaceProviderStatus represents the configuration state of a workspace provider. -type WorkspaceProviderStatus string - -// Workspace Provider statuses. -const ( - WorkspaceProviderPending WorkspaceProviderStatus = "pending" - WorkspaceProviderReady WorkspaceProviderStatus = "ready" -) - -// WorkspaceProviderType represents the type of workspace provider. -type WorkspaceProviderType string - -// Workspace Provider types. -const ( - WorkspaceProviderKubernetes WorkspaceProviderType = "kubernetes" -) - -// WorkspaceProviderByID fetches a workspace provider entity by its unique ID. -func (c *DefaultClient) WorkspaceProviderByID(ctx context.Context, id string) (*KubernetesProvider, error) { - var wp KubernetesProvider - err := c.requestBody(ctx, http.MethodGet, "/api/private/resource-pools/"+id, nil, &wp) - if err != nil { - return nil, err - } - return &wp, nil -} - -// WorkspaceProviders fetches all workspace providers known to the Coder control plane. -func (c *DefaultClient) WorkspaceProviders(ctx context.Context) (*WorkspaceProviders, error) { - var providers WorkspaceProviders - err := c.requestBody(ctx, http.MethodGet, "/api/private/resource-pools", nil, &providers) - if err != nil { - return nil, err - } - return &providers, nil -} - -// CreateWorkspaceProviderReq defines the request parameters for creating a new workspace provider entity. -type CreateWorkspaceProviderReq struct { - Name string `json:"name"` - Type WorkspaceProviderType `json:"type"` - Hostname string `json:"hostname"` - ClusterAddress string `json:"cluster_address"` -} - -// CreateWorkspaceProviderRes defines the response from creating a new workspace provider entity. -type CreateWorkspaceProviderRes struct { - ID string `json:"id" table:"ID"` - Name string `json:"name" table:"Name"` - Status WorkspaceProviderStatus `json:"status" table:"Status"` - EnvproxyToken string `json:"envproxy_token" table:"Envproxy Token"` -} - -// CreateWorkspaceProvider creates a new WorkspaceProvider entity. -func (c *DefaultClient) CreateWorkspaceProvider(ctx context.Context, req CreateWorkspaceProviderReq) (*CreateWorkspaceProviderRes, error) { - var res CreateWorkspaceProviderRes - err := c.requestBody(ctx, http.MethodPost, "/api/private/resource-pools", req, &res) - if err != nil { - return nil, err - } - return &res, nil -} - -// DeleteWorkspaceProviderByID deletes a workspace provider entity from the Coder control plane. -func (c *DefaultClient) DeleteWorkspaceProviderByID(ctx context.Context, id string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/private/resource-pools/"+id, nil, nil) -} - -// CordoneWorkspaceProviderReq defines the request parameters for creating a new workspace provider entity. -type CordoneWorkspaceProviderReq struct { - Reason string `json:"reason"` -} - -// CordonWorkspaceProvider prevents the provider from having any more workspaces placed on it. -func (c *DefaultClient) CordonWorkspaceProvider(ctx context.Context, id, reason string) error { - req := CordoneWorkspaceProviderReq{Reason: reason} - err := c.requestBody(ctx, http.MethodPost, "/api/private/resource-pools/"+id+"/cordon", req, nil) - if err != nil { - return err - } - return nil -} - -// UnCordonWorkspaceProvider changes an existing cordoned providers status to 'Ready'; -// allowing it to continue creating new workspaces and provisioning resources for them. -func (c *DefaultClient) UnCordonWorkspaceProvider(ctx context.Context, id string) error { - err := c.requestBody(ctx, http.MethodPost, "/api/private/resource-pools/"+id+"/uncordon", nil, nil) - if err != nil { - return err - } - return nil -} - -// RenameWorkspaceProviderReq defines the request parameters for changing a workspace provider name. -type RenameWorkspaceProviderReq struct { - Name string `json:"name"` -} - -// RenameWorkspaceProvider changes an existing cordoned providers name field. -func (c *DefaultClient) RenameWorkspaceProvider(ctx context.Context, id string, name string) error { - req := RenameWorkspaceProviderReq{Name: name} - err := c.requestBody(ctx, http.MethodPatch, "/api/private/resource-pools/"+id, req, nil) - if err != nil { - return err - } - return nil -} diff --git a/coder-sdk/ws.go b/coder-sdk/ws.go deleted file mode 100644 index 89cb28e8..00000000 --- a/coder-sdk/ws.go +++ /dev/null @@ -1,35 +0,0 @@ -package coder - -import ( - "context" - "net/http" - - "nhooyr.io/websocket" -) - -// dialWebsocket establish the websocket connection while setting the authentication header. -func (c *DefaultClient) dialWebsocket(ctx context.Context, path string, options ...requestOption) (*websocket.Conn, error) { - // Make a copy of the url so we can update the scheme to ws(s) without mutating the state. - url := *c.baseURL - var config requestOptions - for _, o := range options { - o(&config) - } - if config.BaseURLOverride != nil { - url = *config.BaseURLOverride - } - url.Path = path - - headers := http.Header{} - headers.Set("Session-Token", c.token) - - conn, resp, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{HTTPHeader: headers}) - if err != nil { - if resp != nil { - return nil, NewHTTPError(resp) - } - return nil, err - } - - return conn, nil -} diff --git a/docs/coder.md b/docs/coder.md deleted file mode 100644 index 513efb42..00000000 --- a/docs/coder.md +++ /dev/null @@ -1,27 +0,0 @@ -## coder - -coder provides a CLI for working with an existing Coder installation - -### Options - -``` - -h, --help help for coder - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder completion](coder_completion.md) - Generate completion script -* [coder config-ssh](coder_config-ssh.md) - Configure SSH to access Coder workspaces -* [coder images](coder_images.md) - Manage Coder images -* [coder login](coder_login.md) - Authenticate this client for future operations -* [coder logout](coder_logout.md) - Remove local authentication credentials if any exist -* [coder satellites](coder_satellites.md) - Interact with Coder satellite deployments -* [coder ssh](coder_ssh.md) - Enter a shell of execute a command over SSH into a Coder workspace -* [coder sync](coder_sync.md) - Establish a one way directory sync to a Coder workspace -* [coder tokens](coder_tokens.md) - manage Coder API tokens for the active user -* [coder update](coder_update.md) - Update coder binary -* [coder urls](coder_urls.md) - Interact with workspace DevURLs -* [coder users](coder_users.md) - Interact with Coder user accounts -* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces - diff --git a/docs/coder_completion.md b/docs/coder_completion.md deleted file mode 100644 index 143dcac7..00000000 --- a/docs/coder_completion.md +++ /dev/null @@ -1,70 +0,0 @@ -## coder completion - -Generate completion script - -### Synopsis - -To load completions: - -Bash: - -$ source <(coder completion bash) - -To load completions for each session, execute once: -Linux: - $ coder completion bash > /etc/bash_completion.d/coder -MacOS: - $ coder completion bash > /usr/local/etc/bash_completion.d/coder - -Zsh: - -If shell completion is not already enabled in your workspace you will need -to enable it. You can execute the following once: - -$ echo "autoload -U compinit; compinit" >> ~/.zshrc - -To load completions for each session, execute once: -$ coder completion zsh > "${fpath[1]}/_coder" - -You will need to start a new shell for this setup to take effect. - -Fish: - -$ coder completion fish | source - -To load completions for each session, execute once: -$ coder completion fish > ~/.config/fish/completions/coder.fish - - -``` -coder completion [bash|zsh|fish|powershell] -``` - -### Examples - -``` -coder completion fish > ~/.config/fish/completions/coder.fish -coder completion zsh > "${fpath[1]}/_coder" - -Linux: - $ coder completion bash > /etc/bash_completion.d/coder -MacOS: - $ coder completion bash > /usr/local/etc/bash_completion.d/coder -``` - -### Options - -``` - -h, --help help for completion -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation - diff --git a/docs/coder_config-ssh.md b/docs/coder_config-ssh.md deleted file mode 100644 index 4114239f..00000000 --- a/docs/coder_config-ssh.md +++ /dev/null @@ -1,31 +0,0 @@ -## coder config-ssh - -Configure SSH to access Coder workspaces - -### Synopsis - -Inject the proper OpenSSH configuration into your local SSH config file. - -``` -coder config-ssh [flags] -``` - -### Options - -``` - --filepath string override the default path of your ssh config file (default "~/.ssh/config") - -h, --help help for config-ssh - -o, --option strings additional options injected in the ssh config (ex. disable caching with "-o ControlPath=none") - --remove remove the auto-generated Coder ssh config -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation - diff --git a/docs/coder_images.md b/docs/coder_images.md deleted file mode 100644 index 78c875ed..00000000 --- a/docs/coder_images.md +++ /dev/null @@ -1,26 +0,0 @@ -## coder images - -Manage Coder images - -### Synopsis - -Manage existing images and/or import new ones. - -### Options - -``` - -h, --help help for images - --user string Specifies the user by email (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation -* [coder images ls](coder_images_ls.md) - list all images available to the active user - diff --git a/docs/coder_images_ls.md b/docs/coder_images_ls.md deleted file mode 100644 index bfb646d5..00000000 --- a/docs/coder_images_ls.md +++ /dev/null @@ -1,31 +0,0 @@ -## coder images ls - -list all images available to the active user - -### Synopsis - -List all Coder images available to the active user. - -``` -coder images ls [flags] -``` - -### Options - -``` - -h, --help help for ls - --org string organization name - --output string human | json (default "human") -``` - -### Options inherited from parent commands - -``` - --user string Specifies the user by email (default "me") - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder images](coder_images.md) - Manage Coder images - diff --git a/docs/coder_login.md b/docs/coder_login.md deleted file mode 100644 index ff20bf7e..00000000 --- a/docs/coder_login.md +++ /dev/null @@ -1,24 +0,0 @@ -## coder login - -Authenticate this client for future operations - -``` -coder login [Coder URL eg. https://my.coder.domain/] [flags] -``` - -### Options - -``` - -h, --help help for login -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation - diff --git a/docs/coder_logout.md b/docs/coder_logout.md deleted file mode 100644 index cfb1f4c4..00000000 --- a/docs/coder_logout.md +++ /dev/null @@ -1,24 +0,0 @@ -## coder logout - -Remove local authentication credentials if any exist - -``` -coder logout [flags] -``` - -### Options - -``` - -h, --help help for logout -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation - diff --git a/docs/coder_satellites.md b/docs/coder_satellites.md deleted file mode 100644 index 2eaac5b9..00000000 --- a/docs/coder_satellites.md +++ /dev/null @@ -1,27 +0,0 @@ -## coder satellites - -Interact with Coder satellite deployments - -### Synopsis - -Perform operations on the Coder satellites for the platform. - -### Options - -``` - -h, --help help for satellites -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation -* [coder satellites create](coder_satellites_create.md) - create a new satellite. -* [coder satellites ls](coder_satellites_ls.md) - list satellites. -* [coder satellites rm](coder_satellites_rm.md) - remove a satellite. - diff --git a/docs/coder_satellites_create.md b/docs/coder_satellites_create.md deleted file mode 100644 index 9ab18362..00000000 --- a/docs/coder_satellites_create.md +++ /dev/null @@ -1,36 +0,0 @@ -## coder satellites create - -create a new satellite. - -### Synopsis - -Create a new Coder satellite. - -``` -coder satellites create [name] [satellite_access_url] [flags] -``` - -### Examples - -``` -# create a new satellite - -coder satellites create eu-west https://eu-west.coder.com -``` - -### Options - -``` - -h, --help help for create -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder satellites](coder_satellites.md) - Interact with Coder satellite deployments - diff --git a/docs/coder_satellites_ls.md b/docs/coder_satellites_ls.md deleted file mode 100644 index d2153685..00000000 --- a/docs/coder_satellites_ls.md +++ /dev/null @@ -1,35 +0,0 @@ -## coder satellites ls - -list satellites. - -### Synopsis - -List all Coder workspace satellites. - -``` -coder satellites ls [flags] -``` - -### Examples - -``` -# list satellites -coder satellites ls -``` - -### Options - -``` - -h, --help help for ls -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder satellites](coder_satellites.md) - Interact with Coder satellite deployments - diff --git a/docs/coder_satellites_rm.md b/docs/coder_satellites_rm.md deleted file mode 100644 index 44669f6f..00000000 --- a/docs/coder_satellites_rm.md +++ /dev/null @@ -1,35 +0,0 @@ -## coder satellites rm - -remove a satellite. - -### Synopsis - -Remove an existing Coder satellite by name. - -``` -coder satellites rm [satellite_name] [flags] -``` - -### Examples - -``` -# remove an existing satellite by name -coder satellites rm my-satellite -``` - -### Options - -``` - -h, --help help for rm -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder satellites](coder_satellites.md) - Interact with Coder satellite deployments - diff --git a/docs/coder_ssh.md b/docs/coder_ssh.md deleted file mode 100644 index d57ac50e..00000000 --- a/docs/coder_ssh.md +++ /dev/null @@ -1,31 +0,0 @@ -## coder ssh - -Enter a shell of execute a command over SSH into a Coder workspace - -``` -coder ssh [workspace_name] [] -``` - -### Examples - -``` -coder ssh my-dev -coder ssh my-dev pwd -``` - -### Options - -``` - -h, --help help for ssh -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation - diff --git a/docs/coder_sync.md b/docs/coder_sync.md deleted file mode 100644 index 516f3160..00000000 --- a/docs/coder_sync.md +++ /dev/null @@ -1,25 +0,0 @@ -## coder sync - -Establish a one way directory sync to a Coder workspace - -``` -coder sync [local directory] [:] [flags] -``` - -### Options - -``` - -h, --help help for sync - --init do initial transfer and exit -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation - diff --git a/docs/coder_tokens.md b/docs/coder_tokens.md deleted file mode 100644 index 7b884e5b..00000000 --- a/docs/coder_tokens.md +++ /dev/null @@ -1,29 +0,0 @@ -## coder tokens - -manage Coder API tokens for the active user - -### Synopsis - -Create and manage API Tokens for authenticating the CLI. -Statically authenticate using the token value with the `CODER_TOKEN` and `CODER_URL` workspace variables. - -### Options - -``` - -h, --help help for tokens -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation -* [coder tokens create](coder_tokens_create.md) - create generates a new API token and prints it to stdout -* [coder tokens ls](coder_tokens_ls.md) - show the user's active API tokens -* [coder tokens regen](coder_tokens_regen.md) - regenerate an API token by its unique ID and print the new token to stdout -* [coder tokens rm](coder_tokens_rm.md) - remove an API token by its unique ID - diff --git a/docs/coder_tokens_create.md b/docs/coder_tokens_create.md deleted file mode 100644 index a7a89f54..00000000 --- a/docs/coder_tokens_create.md +++ /dev/null @@ -1,24 +0,0 @@ -## coder tokens create - -create generates a new API token and prints it to stdout - -``` -coder tokens create [token_name] [flags] -``` - -### Options - -``` - -h, --help help for create -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder tokens](coder_tokens.md) - manage Coder API tokens for the active user - diff --git a/docs/coder_tokens_ls.md b/docs/coder_tokens_ls.md deleted file mode 100644 index 6790700d..00000000 --- a/docs/coder_tokens_ls.md +++ /dev/null @@ -1,25 +0,0 @@ -## coder tokens ls - -show the user's active API tokens - -``` -coder tokens ls [flags] -``` - -### Options - -``` - -h, --help help for ls - -o, --output string human | json (default "human") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder tokens](coder_tokens.md) - manage Coder API tokens for the active user - diff --git a/docs/coder_tokens_regen.md b/docs/coder_tokens_regen.md deleted file mode 100644 index 26832102..00000000 --- a/docs/coder_tokens_regen.md +++ /dev/null @@ -1,24 +0,0 @@ -## coder tokens regen - -regenerate an API token by its unique ID and print the new token to stdout - -``` -coder tokens regen [token_id] [flags] -``` - -### Options - -``` - -h, --help help for regen -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder tokens](coder_tokens.md) - manage Coder API tokens for the active user - diff --git a/docs/coder_tokens_rm.md b/docs/coder_tokens_rm.md deleted file mode 100644 index ca95ee0e..00000000 --- a/docs/coder_tokens_rm.md +++ /dev/null @@ -1,24 +0,0 @@ -## coder tokens rm - -remove an API token by its unique ID - -``` -coder tokens rm [token_id] [flags] -``` - -### Options - -``` - -h, --help help for rm -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder tokens](coder_tokens.md) - manage Coder API tokens for the active user - diff --git a/docs/coder_update.md b/docs/coder_update.md deleted file mode 100644 index 8bcc9fae..00000000 --- a/docs/coder_update.md +++ /dev/null @@ -1,31 +0,0 @@ -## coder update - -Update coder binary - -### Synopsis - -Update coder to the version matching a given coder instance. - -``` -coder update [flags] -``` - -### Options - -``` - --coder string query this coder instance for the matching version - --force do not prompt for confirmation - -h, --help help for update - --version string explicitly specify which version to fetch and install -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation - diff --git a/docs/coder_urls.md b/docs/coder_urls.md deleted file mode 100644 index 2bb3e393..00000000 --- a/docs/coder_urls.md +++ /dev/null @@ -1,23 +0,0 @@ -## coder urls - -Interact with workspace DevURLs - -### Options - -``` - -h, --help help for urls -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation -* [coder urls create](coder_urls_create.md) - Create a new dev URL for a workspace -* [coder urls ls](coder_urls_ls.md) - List all DevURLs for a workspace -* [coder urls rm](coder_urls_rm.md) - Remove a dev url - diff --git a/docs/coder_urls_create.md b/docs/coder_urls_create.md deleted file mode 100644 index eb3ed6fa..00000000 --- a/docs/coder_urls_create.md +++ /dev/null @@ -1,33 +0,0 @@ -## coder urls create - -Create a new dev URL for a workspace - -``` -coder urls create [workspace_name] [port] [flags] -``` - -### Examples - -``` -coder urls create my-workspace 8080 --name my-dev-url -``` - -### Options - -``` - --access string Set DevURL access to [private | org | authed | public] (default "private") - -h, --help help for create - --name string DevURL name - --scheme string Server scheme (http|https) (default "http") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder urls](coder_urls.md) - Interact with workspace DevURLs - diff --git a/docs/coder_urls_ls.md b/docs/coder_urls_ls.md deleted file mode 100644 index 79048d9e..00000000 --- a/docs/coder_urls_ls.md +++ /dev/null @@ -1,25 +0,0 @@ -## coder urls ls - -List all DevURLs for a workspace - -``` -coder urls ls [workspace_name] [flags] -``` - -### Options - -``` - -h, --help help for ls - -o, --output string human|json (default "human") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder urls](coder_urls.md) - Interact with workspace DevURLs - diff --git a/docs/coder_urls_rm.md b/docs/coder_urls_rm.md deleted file mode 100644 index 5a25a3bf..00000000 --- a/docs/coder_urls_rm.md +++ /dev/null @@ -1,24 +0,0 @@ -## coder urls rm - -Remove a dev url - -``` -coder urls rm [workspace_name] [port] [flags] -``` - -### Options - -``` - -h, --help help for rm -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder urls](coder_urls.md) - Interact with workspace DevURLs - diff --git a/docs/coder_users.md b/docs/coder_users.md deleted file mode 100644 index 2bfadc7c..00000000 --- a/docs/coder_users.md +++ /dev/null @@ -1,21 +0,0 @@ -## coder users - -Interact with Coder user accounts - -### Options - -``` - -h, --help help for users -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation -* [coder users ls](coder_users_ls.md) - list all user accounts - diff --git a/docs/coder_users_ls.md b/docs/coder_users_ls.md deleted file mode 100644 index ea7b4d4c..00000000 --- a/docs/coder_users_ls.md +++ /dev/null @@ -1,32 +0,0 @@ -## coder users ls - -list all user accounts - -``` -coder users ls [flags] -``` - -### Examples - -``` -coder users ls -o json -coder users ls -o json | jq .[] | jq -r .email -``` - -### Options - -``` - -h, --help help for ls - -o, --output string human | json (default "human") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder users](coder_users.md) - Interact with Coder user accounts - diff --git a/docs/coder_workspaces.md b/docs/coder_workspaces.md deleted file mode 100644 index 936db713..00000000 --- a/docs/coder_workspaces.md +++ /dev/null @@ -1,35 +0,0 @@ -## coder workspaces - -Interact with Coder workspaces - -### Synopsis - -Perform operations on the Coder workspaces owned by the active user. - -### Options - -``` - -h, --help help for workspaces -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation -* [coder workspaces create](coder_workspaces_create.md) - create a new workspace. -* [coder workspaces create-from-config](coder_workspaces_create-from-config.md) - create a new workspace from a template -* [coder workspaces edit](coder_workspaces_edit.md) - edit an existing workspace and initiate a rebuild. -* [coder workspaces edit-from-config](coder_workspaces_edit-from-config.md) - change the template a workspace is tracking -* [coder workspaces ls](coder_workspaces_ls.md) - list all workspaces owned by the active user -* [coder workspaces ping](coder_workspaces_ping.md) - ping Coder workspaces by name -* [coder workspaces policy-template](coder_workspaces_policy-template.md) - Set workspace policy template -* [coder workspaces rebuild](coder_workspaces_rebuild.md) - rebuild a Coder workspace -* [coder workspaces rm](coder_workspaces_rm.md) - remove Coder workspaces by name -* [coder workspaces stop](coder_workspaces_stop.md) - stop Coder workspaces by name -* [coder workspaces watch-build](coder_workspaces_watch-build.md) - trail the build log of a Coder workspace - diff --git a/docs/coder_workspaces_create-from-config.md b/docs/coder_workspaces_create-from-config.md deleted file mode 100644 index 5eed63e9..00000000 --- a/docs/coder_workspaces_create-from-config.md +++ /dev/null @@ -1,43 +0,0 @@ -## coder workspaces create-from-config - -create a new workspace from a template - -### Synopsis - -Create a new Coder workspace using a workspace template. - -``` -coder workspaces create-from-config [flags] -``` - -### Examples - -``` -# create a new workspace from git repository -coder workspaces create-from-config --name="dev-env" --repo-url https://github.com/cdr/m --ref my-branch -coder workspaces create-from-config --name="dev-env" --filepath coder.yaml -``` - -### Options - -``` - -f, --filepath string path to local template file. - --follow follow buildlog after initiating rebuild - -h, --help help for create-from-config - --name string name of the workspace to be created - -o, --org string name of the organization the workspace should be created under. - --provider string name of Workspace Provider with which to create the workspace - --ref string git reference to pull template from. May be a branch, tag, or commit hash. (default "master") - -r, --repo-url string URL of the git repository to pull the config from. Config file must live in '.coder/coder.yaml'. -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces - diff --git a/docs/coder_workspaces_create.md b/docs/coder_workspaces_create.md deleted file mode 100644 index ab4f8731..00000000 --- a/docs/coder_workspaces_create.md +++ /dev/null @@ -1,48 +0,0 @@ -## coder workspaces create - -create a new workspace. - -### Synopsis - -Create a new Coder workspace. - -``` -coder workspaces create [workspace_name] [flags] -``` - -### Examples - -``` -# create a new workspace using default resource amounts -coder workspaces create my-new-workspace --image ubuntu -coder workspaces create my-new-powerful-workspace --cpu 12 --disk 100 --memory 16 --image ubuntu -``` - -### Options - -``` - --container-based-vm deploy the workspace as a Container-based VM - -c, --cpu float32 number of cpu cores the workspace should be provisioned with. - -d, --disk int GB of disk storage a workspace should be provisioned with. - --enable-autostart automatically start this workspace at your preferred time. - --follow follow buildlog after initiating rebuild - -g, --gpus int number GPUs a workspace should be provisioned with. - -h, --help help for create - -i, --image string name of the image to base the workspace off of. - -m, --memory float32 GB of RAM a workspace should be provisioned with. - -o, --org string name of the organization the workspace should be created under. - --provider string name of Workspace Provider with which to create the workspace - -t, --tag string tag of the image the workspace will be based off of. (default "latest") - --user string Specify the user whose resources to target. This flag can only be used by admins and managers. Input an email or user id. (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces - diff --git a/docs/coder_workspaces_edit-from-config.md b/docs/coder_workspaces_edit-from-config.md deleted file mode 100644 index 31c68cb3..00000000 --- a/docs/coder_workspaces_edit-from-config.md +++ /dev/null @@ -1,38 +0,0 @@ -## coder workspaces edit-from-config - -change the template a workspace is tracking - -### Synopsis - -Edit an existing Coder workspace using a workspace template. - -``` -coder workspaces edit-from-config [flags] -``` - -### Examples - -``` -# edit a new workspace from git repository -coder workspaces edit-from-config dev-env --repo-url https://github.com/cdr/m --ref my-branch -coder workspaces edit-from-config dev-env --filepath coder.yaml -``` - -### Options - -``` - -f, --filepath string path to local template file. - --follow follow buildlog after initiating rebuild - -h, --help help for edit-from-config -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces - diff --git a/docs/coder_workspaces_edit.md b/docs/coder_workspaces_edit.md deleted file mode 100644 index 2d214cb4..00000000 --- a/docs/coder_workspaces_edit.md +++ /dev/null @@ -1,46 +0,0 @@ -## coder workspaces edit - -edit an existing workspace and initiate a rebuild. - -### Synopsis - -Edit an existing workspace and initate a rebuild. - -``` -coder workspaces edit [flags] -``` - -### Examples - -``` -coder workspaces edit back-end-workspace --cpu 4 - -coder workspaces edit back-end-workspace --disk 20 -``` - -### Options - -``` - -c, --cpu float32 The number of cpu cores the workspace should be provisioned with. - -d, --disk int The amount of disk storage a workspace should be provisioned with. - --follow follow buildlog after initiating rebuild - --force force rebuild without showing a confirmation prompt - -g, --gpu int The amount of disk storage to provision the workspace with. - -h, --help help for edit - -i, --image string name of the image you want the workspace to be based off of. - -m, --memory float32 The amount of RAM a workspace should be provisioned with. - -o, --org string name of the organization the workspace should be created under. - -t, --tag string image tag of the image you want to base the workspace off of. (default "latest") - --user string Specify the user whose resources to target (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces - diff --git a/docs/coder_workspaces_ls.md b/docs/coder_workspaces_ls.md deleted file mode 100644 index 71ee02d9..00000000 --- a/docs/coder_workspaces_ls.md +++ /dev/null @@ -1,32 +0,0 @@ -## coder workspaces ls - -list all workspaces owned by the active user - -### Synopsis - -List all Coder workspaces owned by the active user. - -``` -coder workspaces ls [flags] -``` - -### Options - -``` - --all Get workspaces for all users (admin only) - -h, --help help for ls - -o, --output string human | json (default "human") - -p, --provider string Filter workspaces by a particular workspace provider name. - --user string Specify the user whose resources to target (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces - diff --git a/docs/coder_workspaces_ping.md b/docs/coder_workspaces_ping.md deleted file mode 100644 index cf55fff1..00000000 --- a/docs/coder_workspaces_ping.md +++ /dev/null @@ -1,36 +0,0 @@ -## coder workspaces ping - -ping Coder workspaces by name - -### Synopsis - -ping Coder workspaces by name - -``` -coder workspaces ping [flags] -``` - -### Examples - -``` -coder workspaces ping front-end-workspace -``` - -### Options - -``` - -c, --count int stop after replies - -h, --help help for ping - -s, --scheme strings customize schemes to filter ice servers (default [stun,stuns,turn,turns]) -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces - diff --git a/docs/coder_workspaces_policy-template.md b/docs/coder_workspaces_policy-template.md deleted file mode 100644 index 36bf34fc..00000000 --- a/docs/coder_workspaces_policy-template.md +++ /dev/null @@ -1,32 +0,0 @@ -## coder workspaces policy-template - -Set workspace policy template - -### Synopsis - -Set workspace policy template or restore to default configuration. This feature is for site admins only. - -``` -coder workspaces policy-template [flags] -``` - -### Options - -``` - --default Restore policy template to default configuration - --dry-run skip setting policy template, but view errors/warnings about how this policy template would impact existing workspaces - -f, --filepath string full path to local policy template file. - -h, --help help for policy-template - --scope string scope of impact for the policy template. Supported values: site (default "site") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces - diff --git a/docs/coder_workspaces_rebuild.md b/docs/coder_workspaces_rebuild.md deleted file mode 100644 index 8e928852..00000000 --- a/docs/coder_workspaces_rebuild.md +++ /dev/null @@ -1,34 +0,0 @@ -## coder workspaces rebuild - -rebuild a Coder workspace - -``` -coder workspaces rebuild [workspace_name] [flags] -``` - -### Examples - -``` -coder workspaces rebuild front-end-workspace --follow -coder workspaces rebuild backend-workspace --force -``` - -### Options - -``` - --follow follow build log after initiating rebuild - --force force rebuild without showing a confirmation prompt - -h, --help help for rebuild - --user string Specify the user whose resources to target (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces - diff --git a/docs/coder_workspaces_rm.md b/docs/coder_workspaces_rm.md deleted file mode 100644 index b19ece2d..00000000 --- a/docs/coder_workspaces_rm.md +++ /dev/null @@ -1,26 +0,0 @@ -## coder workspaces rm - -remove Coder workspaces by name - -``` -coder workspaces rm [...workspace_names] [flags] -``` - -### Options - -``` - -f, --force force remove the specified workspaces without prompting first - -h, --help help for rm - --user string Specify the user whose resources to target (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces - diff --git a/docs/coder_workspaces_stop.md b/docs/coder_workspaces_stop.md deleted file mode 100644 index 0e00ecef..00000000 --- a/docs/coder_workspaces_stop.md +++ /dev/null @@ -1,44 +0,0 @@ -## coder workspaces stop - -stop Coder workspaces by name - -### Synopsis - -Stop Coder workspaces by name - -``` -coder workspaces stop [...workspace_names] [flags] -``` - -### Examples - -``` -coder workspaces stop front-end-workspace -coder workspaces stop front-end-workspace backend-workspace - -# stop all of your workspaces -coder workspaces ls -o json | jq -c '.[].name' | xargs coder workspaces stop - -# stop all workspaces for a given user -coder workspaces --user charlie@coder.com ls -o json \ - | jq -c '.[].name' \ - | xargs coder workspaces --user charlie@coder.com stop -``` - -### Options - -``` - -h, --help help for stop - --user string Specify the user whose resources to target (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces - diff --git a/docs/coder_workspaces_watch-build.md b/docs/coder_workspaces_watch-build.md deleted file mode 100644 index 8af34e82..00000000 --- a/docs/coder_workspaces_watch-build.md +++ /dev/null @@ -1,31 +0,0 @@ -## coder workspaces watch-build - -trail the build log of a Coder workspace - -``` -coder workspaces watch-build [workspace_name] [flags] -``` - -### Examples - -``` -coder workspaces watch-build front-end-workspace -``` - -### Options - -``` - -h, --help help for watch-build - --user string Specify the user whose resources to target (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces - diff --git a/go.mod b/go.mod deleted file mode 100644 index 702db277..00000000 --- a/go.mod +++ /dev/null @@ -1,42 +0,0 @@ -module cdr.dev/coder-cli - -go 1.14 - -// TODO: remove the replace once this PR gets merged: -// https://github.com/pion/webrtc/pull/1946 -replace github.com/pion/webrtc/v3 => github.com/deansheather/webrtc/v3 v3.1.0-beta.6.0.20210907233552-57c66b872d12 - -require ( - cdr.dev/slog v1.4.1 - cdr.dev/wsep v0.1.0 - github.com/Masterminds/semver/v3 v3.1.1 - github.com/briandowns/spinner v1.16.0 - github.com/cli/safeexec v1.0.0 - github.com/fatih/color v1.12.0 - github.com/google/go-cmp v0.5.6 - github.com/gorilla/websocket v1.4.2 - github.com/hashicorp/yamux v0.0.0-20210826001029-26ff87cf9493 - github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f - github.com/klauspost/compress v1.13.5 // indirect - github.com/manifoldco/promptui v0.8.0 - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/pion/datachannel v1.4.21 - github.com/pion/dtls/v2 v2.0.9 - github.com/pion/ice/v2 v2.1.12 - github.com/pion/logging v0.2.2 - github.com/pion/turn/v2 v2.0.5 - github.com/pion/webrtc/v3 v3.1.0-beta.7 - github.com/pkg/browser v0.0.0-20210904010418-6d279e18f982 - github.com/rjeczalik/notify v0.9.2 - github.com/spf13/afero v1.6.0 - github.com/spf13/cobra v1.2.1 - github.com/stretchr/testify v1.7.0 - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 - golang.org/x/net v0.0.0-20210907225631-ff17edfbf26d - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 - golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b - golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 - nhooyr.io/websocket v1.8.7 -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 908f7bcf..00000000 --- a/go.sum +++ /dev/null @@ -1,819 +0,0 @@ -cdr.dev/slog v1.3.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns= -cdr.dev/slog v1.4.1 h1:Q8+X63m8/WB4geelMTDO8t4CTwVh1f7+5Cxi7kS/SZg= -cdr.dev/slog v1.4.1/go.mod h1:O76C6gZJxa5HK1SXMrjd48V2kJxYZKFRTcFfn/V9OhA= -cdr.dev/wsep v0.1.0 h1:70z0VLd9hCHuBrX/XLmhspRDL0XTAtpg/0tGqHiEIhY= -cdr.dev/wsep v0.1.0/go.mod h1:9xXBG4xn80ogx5+WHX0zlFZR2X28ECbGtakqU7ncjdM= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0 h1:bAMqZidYkmIsUqe6PtkEPT7Q+vfizScn+jfNA6jwK9c= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= -github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= -github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= -github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY= -github.com/alecthomas/chroma v0.9.1 h1:cBmvQqRImzR5aWqdMxYZByND4S7BCS/g0svZb28h0Dc= -github.com/alecthomas/chroma v0.9.1/go.mod h1:eMuEnpA18XbG/WhOWtCzJHS7WqEtDAI+HxdwoW0nVSk= -github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= -github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= -github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= -github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= -github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= -github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= -github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= -github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/briandowns/spinner v1.16.0 h1:DFmp6hEaIx2QXXuqSJmtfSBSAjRmpGiKG6ip2Wm/yOs= -github.com/briandowns/spinner v1.16.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= -github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= -github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= -github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deansheather/webrtc/v3 v3.1.0-beta.6.0.20210907233552-57c66b872d12 h1:NW5uk8N+M5hp5TOTcGi9a5/ZFyeG8sTVQIAKZFAWhJM= -github.com/deansheather/webrtc/v3 v3.1.0-beta.6.0.20210907233552-57c66b872d12/go.mod h1:KQH/wVKKJzBTQ6sX1bDTxsvTpQ6gEjVZSJlzYaB58aM= -github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= -github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= -github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= -github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/yamux v0.0.0-20210826001029-26ff87cf9493 h1:brI5vBRUlAlM34VFmnLPwjnCL/FxAJp9XvOdX6Zt+XE= -github.com/hashicorp/yamux v0.0.0-20210826001029-26ff87cf9493/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= -github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= -github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU= -github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.13.5 h1:9O69jUPDcsT9fEm74W92rZL9FQY7rCdaXVneq+yyzl4= -github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= -github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo= -github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0= -github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= -github.com/pion/dtls/v2 v2.0.9 h1:7Ow+V++YSZQMYzggI0P9vLJz/hUFcffsfGMfT/Qy+u8= -github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQKYQho= -github.com/pion/ice/v2 v2.1.12 h1:ZDBuZz+fEI7iDifZCYFVzI4p0Foy0YhdSSZ87ZtRcRE= -github.com/pion/ice/v2 v2.1.12/go.mod h1:ovgYHUmwYLlRvcCLI67PnQ5YGe+upXZbGgllBDG/ktU= -github.com/pion/interceptor v0.0.15 h1:pQFkBUL8akUHiGoFr+pM94Q/15x7sLFh0K3Nj+DCC6s= -github.com/pion/interceptor v0.0.15/go.mod h1:pg3J253eGi5bqyKzA74+ej5Y19ez2jkWANVnF+Z9Dfk= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw= -github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g= -github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= -github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo= -github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= -github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= -github.com/pion/rtp v1.7.2 h1:HCDKDCixh7PVjkQTsqHAbk1lg+bx059EHxcnyl42dYs= -github.com/pion/rtp v1.7.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= -github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= -github.com/pion/sctp v1.7.12 h1:GsatLufywVruXbZZT1CKg+Jr8ZTkwiPnmUC/oO9+uuY= -github.com/pion/sctp v1.7.12/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= -github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8= -github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= -github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ= -github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A= -github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= -github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= -github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= -github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= -github.com/pion/transport v0.12.3 h1:vdBfvfU/0Wq8kd2yhUMSDB/x+O4Z9MYVl2fJ5BT4JZw= -github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A= -github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= -github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= -github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= -github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= -github.com/pkg/browser v0.0.0-20210904010418-6d279e18f982 h1:TdFv+3Gr3GaghJ/o80aulO4ian7GHGWMdLBXoLZH1Is= -github.com/pkg/browser v0.0.0-20210904010418-6d279e18f982/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8= -github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.coder.com/cli v0.4.0/go.mod h1:hRTOURCR3LJF1FRW9arecgrzX+AHG7mfYMwThPIgq+w= -go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512 h1:DjCS6dRQh+1PlfiBmnabxfdrzenb0tAwJqFxDEH/s9g= -go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512/go.mod h1:83JsYgXYv0EOaXjIMnaZ1Fl6ddNB3fJnDZ/8845mUJ8= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210907225631-ff17edfbf26d h1:kuk8nKPQ25KCDODLCDXt99tnTVeOyOM8HGvtJ0NzAvw= -golang.org/x/net v0.0.0-20210907225631-ff17edfbf26d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 h1:GkvMjFtXUmahfDtashnc1mnrCtuBVcwse5QV2lUk/tI= -golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= -nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= -nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/activity/pusher.go b/internal/activity/pusher.go deleted file mode 100644 index f432c72e..00000000 --- a/internal/activity/pusher.go +++ /dev/null @@ -1,47 +0,0 @@ -package activity - -import ( - "context" - "fmt" - "time" - - "golang.org/x/time/rate" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/clog" -) - -const pushInterval = time.Minute - -// Pusher pushes activity metrics no more than once per pushInterval. Pushes -// within the same interval are a no-op. -type Pusher struct { - workspaceID string - source string - - client coder.Client - rate *rate.Limiter // Use a rate limiter to control the sampling rate. -} - -// NewPusher instantiates a new instance of Pusher. -func NewPusher(c coder.Client, workspaceID, source string) *Pusher { - return &Pusher{ - workspaceID: workspaceID, - source: source, - client: c, - // Sample only 1 per interval to avoid spamming the api. - rate: rate.NewLimiter(rate.Every(pushInterval), 1), - } -} - -// Push pushes activity, abiding by a rate limit. -func (p *Pusher) Push(ctx context.Context) { - // If we already sampled data within the allowable range, do nothing. - if !p.rate.Allow() { - return - } - - if err := p.client.PushActivity(ctx, p.source, p.workspaceID); err != nil { - clog.Log(clog.Error(fmt.Sprintf("push activity: %s", err))) - } -} diff --git a/internal/activity/writer.go b/internal/activity/writer.go deleted file mode 100644 index 02d9d1b8..00000000 --- a/internal/activity/writer.go +++ /dev/null @@ -1,24 +0,0 @@ -// Package activity defines the logic for tracking usage activity metrics. -package activity - -import ( - "context" - "io" -) - -// writer wraps a standard io.Writer with the activity pusher. -type writer struct { - p *Pusher - wr io.Writer -} - -// Write writes to the underlying writer and tracks activity. -func (w *writer) Write(buf []byte) (int, error) { - w.p.Push(context.Background()) - return w.wr.Write(buf) -} - -// Writer wraps the given writer such that all writes trigger an activity push. -func (p *Pusher) Writer(wr io.Writer) io.Writer { - return &writer{p: p, wr: wr} -} diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go deleted file mode 100644 index 9b769c13..00000000 --- a/internal/cmd/agent.go +++ /dev/null @@ -1,127 +0,0 @@ -package cmd - -import ( - "net/url" - "os" - "os/signal" - "syscall" - - // We use slog here since agent runs in the background and we can benefit - // from structured logging. - "cdr.dev/slog" - "cdr.dev/slog/sloggers/sloghuman" - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/wsnet" -) - -func agentCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "agent", - Short: "Run the workspace agent", - Long: "Connect to Coder and start running a p2p agent", - Hidden: true, - } - - cmd.AddCommand( - startCmd(), - ) - return cmd -} - -func startCmd() *cobra.Command { - var ( - token string - coderURL string - logFile string - ) - cmd := &cobra.Command{ - Use: "start --coder-url= --token= --log-file=", - Short: "starts the coder agent", - Long: "starts the coder agent", - Example: `# start the agent and use CODER_URL and CODER_AGENT_TOKEN env vars -coder agent start - -# start the agent and connect with a specified url and agent token -coder agent start --coder-url https://my-coder.com --token xxxx-xxxx - -# start the agent and write a copy of the log to /tmp/coder-agent.log -# if the file already exists, it will be truncated -coder agent start --log-file=/tmp/coder-agent.log -`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - log := slog.Make(sloghuman.Sink(os.Stderr)).Leveled(slog.LevelDebug) - - // Optional log file path to write - if logFile != "" { - // Truncate the file if it already exists - file, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - // If an error occurs, log it as an error, but consider it non-fatal - log.Warn(ctx, "failed to open log file", slog.Error(err)) - } else { - // Log to both standard output and our file - log = slog.Make( - sloghuman.Sink(os.Stderr), - sloghuman.Sink(file), - ).Leveled(slog.LevelDebug) - } - } - - if coderURL == "" { - var ok bool - coderURL, ok = os.LookupEnv("CODER_URL") - if !ok { - client, err := newClient(ctx, true) - if err != nil { - return xerrors.New("must login, pass --coder-url flag, or set the CODER_URL env variable") - } - burl := client.BaseURL() - coderURL = burl.String() - } - } - - u, err := url.Parse(coderURL) - if err != nil { - return xerrors.Errorf("parse url: %w", err) - } - - if token == "" { - var ok bool - token, ok = os.LookupEnv("CODER_AGENT_TOKEN") - if !ok { - return xerrors.New("must pass --token or set the CODER_AGENT_TOKEN env variable") - } - } - - log.Info(ctx, "starting wsnet listener", slog.F("coder_access_url", u.String())) - listener, err := wsnet.Listen(ctx, log, wsnet.ListenEndpoint(u, token), token) - if err != nil { - return xerrors.Errorf("listen: %w", err) - } - defer func() { - log.Info(ctx, "closing wsnet listener") - err := listener.Close() - if err != nil { - log.Error(ctx, "close listener", slog.Error(err)) - } - }() - - // Block until user sends SIGINT or SIGTERM - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - <-sigs - - return nil - }, - } - - cmd.Flags().StringVar(&token, "token", "", "coder agent token") - cmd.Flags().StringVar(&coderURL, "coder-url", "", "coder access url") - cmd.Flags().StringVar(&logFile, "log-file", "", "write a copy of logs to file") - - return cmd -} diff --git a/internal/cmd/auth.go b/internal/cmd/auth.go deleted file mode 100644 index d2b8201b..00000000 --- a/internal/cmd/auth.go +++ /dev/null @@ -1,87 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "net/http" - "net/url" - "os" - - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/config" - "cdr.dev/coder-cli/internal/version" - "cdr.dev/coder-cli/pkg/clog" -) - -var errNeedLogin = clog.Fatal( - "failed to read session credentials", - clog.Hintf(`did you run "coder login [https://coder.domain.com]"?`), -) - -const tokenEnv = "CODER_TOKEN" -const urlEnv = "CODER_URL" - -func newClient(ctx context.Context, checkVersion bool) (coder.Client, error) { - var ( - err error - sessionToken = os.Getenv(tokenEnv) - rawURL = os.Getenv(urlEnv) - ) - - if sessionToken == "" || rawURL == "" { - sessionToken, err = config.Session.Read() - if err != nil { - return nil, errNeedLogin - } - - rawURL, err = config.URL.Read() - if err != nil { - return nil, errNeedLogin - } - } - - u, err := url.Parse(rawURL) - if err != nil { - return nil, xerrors.Errorf("url malformed: %w try running \"coder login\" with a valid URL", err) - } - - c, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - Token: sessionToken, - }) - if err != nil { - return nil, xerrors.Errorf("failed to create new coder.Client: %w", err) - } - - if checkVersion { - var apiVersion string - apiVersion, err = c.APIVersion(ctx) - if apiVersion != "" && !version.VersionsMatch(apiVersion) { - logVersionMismatchError(apiVersion) - } - } - - if err != nil { - var he *coder.HTTPError - if xerrors.As(err, &he) { - if he.StatusCode() == http.StatusUnauthorized { - return nil, xerrors.Errorf("not authenticated: try running \"coder login`\"") - } - } - return nil, err - } - - return c, nil -} - -func logVersionMismatchError(apiVersion string) { - clog.LogWarn( - "version mismatch detected", - fmt.Sprintf("Coder CLI version: %s", version.Version), - fmt.Sprintf("Coder API version: %s", apiVersion), clog.BlankLine, - clog.Tipf("download the appropriate version here: https://github.com/cdr/coder-cli/releases"), - clog.Tipf("alternatively, run `coder update`"), - ) -} diff --git a/internal/cmd/ceapi.go b/internal/cmd/ceapi.go deleted file mode 100644 index 0c5ca004..00000000 --- a/internal/cmd/ceapi.go +++ /dev/null @@ -1,243 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "strings" - - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/coderutil" - "cdr.dev/coder-cli/pkg/clog" -) - -// Helpers for working with the Coder API. - -// lookupUserOrgs gets a list of orgs the user is apart of. -func lookupUserOrgs(user *coder.User, orgs []coder.Organization) []coder.Organization { - // NOTE: We don't know in advance how many orgs the user is in so we can't pre-alloc. - var userOrgs []coder.Organization - - for _, org := range orgs { - for _, member := range org.Members { - if member.ID != user.ID { - continue - } - // If we found the user in the org, add it to the list and skip to the next org. - userOrgs = append(userOrgs, org) - break - } - } - return userOrgs -} - -// getAllWorkspaces gets all workspaces for all users, on all providers. -func getAllWorkspaces(ctx context.Context, client coder.Client) ([]coder.Workspace, error) { - return client.Workspaces(ctx) -} - -// getWorkspaces returns all workspaces for the user. -func getWorkspaces(ctx context.Context, client coder.Client, email string) ([]coder.Workspace, error) { - user, err := client.UserByEmail(ctx, email) - if err != nil { - return nil, xerrors.Errorf("get user: %w", err) - } - - orgs, err := client.Organizations(ctx) - if err != nil { - return nil, xerrors.Errorf("get orgs: %w", err) - } - - orgs = lookupUserOrgs(user, orgs) - - // NOTE: We don't know in advance how many workspaces we have so we can't pre-alloc. - var allWorkspaces []coder.Workspace - - for _, org := range orgs { - workspaces, err := client.UserWorkspacesByOrganization(ctx, user.ID, org.ID) - if err != nil { - return nil, xerrors.Errorf("get workspaces for %s: %w", org.Name, err) - } - - allWorkspaces = append(allWorkspaces, workspaces...) - } - return allWorkspaces, nil -} - -// searchForWorkspace searches a user's workspaces to find the specified workspaceName. If none is found, the haystack of -// workspace names is returned. -func searchForWorkspace(ctx context.Context, client coder.Client, workspaceName, userEmail string) (_ *coder.Workspace, haystack []string, _ error) { - workspaces, err := getWorkspaces(ctx, client, userEmail) - if err != nil { - return nil, nil, xerrors.Errorf("get workspaces: %w", err) - } - - // NOTE: We don't know in advance where we will find the workspace, so we can't pre-alloc. - for _, workspace := range workspaces { - if workspace.Name == workspaceName { - return &workspace, nil, nil - } - // Keep track of what we found for the logs. - haystack = append(haystack, workspace.Name) - } - return nil, haystack, coder.ErrNotFound -} - -// findWorkspace returns a single workspace by name (if it exists.). -func findWorkspace(ctx context.Context, client coder.Client, workspaceName, userEmail string) (*coder.Workspace, error) { - workspace, haystack, err := searchForWorkspace(ctx, client, workspaceName, userEmail) - if err != nil { - return nil, clog.Fatal( - "failed to find workspace", - fmt.Sprintf("workspace %q not found in %q", workspaceName, haystack), - clog.BlankLine, - clog.Tipf("run \"coder workspaces ls\" to view your workspaces"), - ) - } - return workspace, nil -} - -type findImgConf struct { - email string - imgName string - orgName string -} - -func findImg(ctx context.Context, client coder.Client, conf findImgConf) (*coder.Image, error) { - switch { - case conf.email == "": - return nil, xerrors.New("user email unset") - case conf.imgName == "": - return nil, xerrors.New("image name unset") - } - - imgs, err := getImgs(ctx, client, getImgsConf{ - email: conf.email, - orgName: conf.orgName, - }) - if err != nil { - return nil, err - } - - var possibleMatches []coder.Image - - // The user may provide an image thats not an exact match - // to one of their imported images but they may be close. - // We can assist the user by collecting images that contain - // the user provided image flag value as a substring. - for _, img := range imgs { - // If it's an exact match we can just return and exit. - if img.Repository == conf.imgName { - return &img, nil - } - if strings.Contains(img.Repository, conf.imgName) { - possibleMatches = append(possibleMatches, img) - } - } - - if len(possibleMatches) == 0 { - return nil, xerrors.New("image not found - did you forget to import this image?") - } - - lines := []string{clog.Hintf("Did you mean?")} - - for _, img := range possibleMatches { - lines = append(lines, fmt.Sprintf(" %s", img.Repository)) - } - return nil, clog.Fatal( - fmt.Sprintf("image %s not found", conf.imgName), - lines..., - ) -} - -type getImgsConf struct { - email string - orgName string -} - -func getImgs(ctx context.Context, client coder.Client, conf getImgsConf) ([]coder.Image, error) { - u, err := client.UserByEmail(ctx, conf.email) - if err != nil { - return nil, err - } - - orgs, err := client.Organizations(ctx) - if err != nil { - return nil, err - } - - orgs = lookupUserOrgs(u, orgs) - - for _, org := range orgs { - imgs, err := client.OrganizationImages(ctx, org.ID) - if err != nil { - return nil, err - } - // If orgName is set we know the user is a multi-org member - // so we should only return the imported images that beong to the org they specified. - if conf.orgName != "" && conf.orgName == org.Name { - return imgs, nil - } - - if conf.orgName == "" { - // if orgName is unset we know the user is only part of one org. - return imgs, nil - } - } - return nil, xerrors.Errorf("org name %q not found", conf.orgName) -} - -func isMultiOrgMember(ctx context.Context, client coder.Client, email string) (bool, error) { - orgs, err := getUserOrgs(ctx, client, email) - if err != nil { - return false, err - } - return len(orgs) > 1, nil -} - -func getUserOrgs(ctx context.Context, client coder.Client, email string) ([]coder.Organization, error) { - u, err := client.UserByEmail(ctx, email) - if err != nil { - return nil, xerrors.New("email not found") - } - - orgs, err := client.Organizations(ctx) - if err != nil { - return nil, xerrors.New("no organizations found") - } - return lookupUserOrgs(u, orgs), nil -} - -func getWorkspacesByProvider(ctx context.Context, client coder.Client, wpName, userEmail string) ([]coder.Workspace, error) { - wp, err := coderutil.ProviderByName(ctx, client, wpName) - if err != nil { - return nil, err - } - - workspaces, err := client.WorkspacesByWorkspaceProvider(ctx, wp.ID) - if err != nil { - return nil, err - } - - workspaces, err = filterWorkspacesByUser(ctx, client, userEmail, workspaces) - if err != nil { - return nil, err - } - return workspaces, nil -} - -func filterWorkspacesByUser(ctx context.Context, client coder.Client, userEmail string, workspaces []coder.Workspace) ([]coder.Workspace, error) { - user, err := client.UserByEmail(ctx, userEmail) - if err != nil { - return nil, xerrors.Errorf("get user: %w", err) - } - - var filteredWorkspaces []coder.Workspace - for _, workspace := range workspaces { - if workspace.UserID == user.ID { - filteredWorkspaces = append(filteredWorkspaces, workspace) - } - } - return filteredWorkspaces, nil -} diff --git a/internal/cmd/cli_test.go b/internal/cmd/cli_test.go deleted file mode 100644 index ca145a9d..00000000 --- a/internal/cmd/cli_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package cmd - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/url" - "os" - "strings" - "testing" - - "cdr.dev/slog" - "cdr.dev/slog/sloggers/slogtest" - "cdr.dev/slog/sloggers/slogtest/assert" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/config" - "cdr.dev/coder-cli/pkg/clog" -) - -var ( - shouldSkipAuthedTests bool = false - testCoderClient coder.Client -) - -func isCI() bool { _, ok := os.LookupEnv("CI"); return ok } - -func skipIfNoAuth(t *testing.T) { - if shouldSkipAuthedTests { - t.Skip("no authentication provided and not in CI, skipping") - } -} - -func init() { - tmpDir, err := ioutil.TempDir("", "coder-cli-config-dir") - if err != nil { - panic(err) - } - config.SetRoot(tmpDir) - - email := os.Getenv("CODER_EMAIL") - password := os.Getenv("CODER_PASSWORD") - rawURL := os.Getenv("CODER_URL") - if email == "" || password == "" || rawURL == "" { - if isCI() { - panic("when run in CI, CODER_EMAIL, CODER_PASSWORD, and CODER_URL are required environment variables") - } - shouldSkipAuthedTests = true - return - } - u, err := url.Parse(rawURL) - if err != nil { - panic("invalid CODER_URL: " + err.Error()) - } - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - Email: email, - Password: password, - }) - if err != nil { - panic("new client: " + err.Error()) - } - testCoderClient = client - if err := config.URL.Write(rawURL); err != nil { - panic("write config url: " + err.Error()) - } - if err := config.Session.Write(client.Token()); err != nil { - panic("write config token: " + err.Error()) - } -} - -type result struct { - outBuffer *bytes.Buffer - errBuffer *bytes.Buffer - exitErr error -} - -func (r result) success(t *testing.T) { - t.Helper() - assert.Success(t, "execute command", r.exitErr) -} - -func (r result) error(t *testing.T) { - t.Helper() - assert.Error(t, "execute command", r.exitErr) -} - -//nolint -func (r result) stdoutContains(t *testing.T, substring string) { - t.Helper() - if !strings.Contains(r.outBuffer.String(), substring) { - slogtest.Fatal(t, "stdout contains substring", slog.F("substring", substring), slog.F("stdout", r.outBuffer.String())) - } -} - -func (r result) stdoutUnmarshals(t *testing.T, target interface{}) { - t.Helper() - err := json.Unmarshal(r.outBuffer.Bytes(), target) - assert.Success(t, "unmarshal json", err) -} - -//nolint -func (r result) stdoutEmpty(t *testing.T) { - t.Helper() - assert.Equal(t, "stdout empty", "", r.outBuffer.String()) -} - -//nolint -func (r result) stderrEmpty(t *testing.T) { - t.Helper() - assert.Equal(t, "stderr empty", "", r.errBuffer.String()) -} - -//nolint -func (r result) stderrContains(t *testing.T, substring string) { - t.Helper() - if !strings.Contains(r.errBuffer.String(), substring) { - slogtest.Fatal(t, "stderr contains substring", slog.F("substring", substring), slog.F("stderr", r.errBuffer.String())) - } -} - -//nolint -func (r result) clogError(t *testing.T) clog.CLIError { - t.Helper() - var cliErr clog.CLIError - if !xerrors.As(r.exitErr, &cliErr) { - slogtest.Fatal(t, "expected clog error, none found", slog.Error(r.exitErr), slog.F("type", fmt.Sprintf("%T", r.exitErr))) - } - slogtest.Debug(t, "clog error", slog.F("message", cliErr.String())) - return cliErr -} - -//nolint -func execute(t *testing.T, in io.Reader, args ...string) result { - cmd := Make() - - var outStream bytes.Buffer - var errStream bytes.Buffer - - cmd.SetArgs(args) - - cmd.SetIn(in) - cmd.SetOut(&outStream) - cmd.SetErr(&errStream) - - // TODO: this *needs* to be moved to function scoped writer arg. As is, - // this prevents tests from running in parallel. - clog.SetOutput(&errStream) - - err := cmd.Execute() - - slogtest.Debug(t, "execute command", - slog.F("out_buffer", outStream.String()), - slog.F("err_buffer", errStream.String()), - slog.F("args", args), - slog.F("execute_error", err), - ) - if err != nil { - clog.Log(err) - } - return result{ - outBuffer: &outStream, - errBuffer: &errStream, - exitErr: err, - } -} diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go deleted file mode 100644 index 90911c07..00000000 --- a/internal/cmd/cmd.go +++ /dev/null @@ -1,122 +0,0 @@ -// Package cmd constructs all subcommands for coder-cli. -package cmd - -import ( - "github.com/spf13/cobra" - "github.com/spf13/cobra/doc" - - "cdr.dev/coder-cli/internal/x/xcobra" -) - -// verbose is a global flag for specifying that a command should give verbose output. -var verbose bool = false - -// Make constructs the "coder" root command. -func Make() *cobra.Command { - app := &cobra.Command{ - Use: "coder", - Short: "coder provides a CLI for working with an existing Coder installation", - SilenceErrors: true, - SilenceUsage: true, - DisableAutoGenTag: true, - } - - app.AddCommand( - agentCmd(), - completionCmd(), - configSSHCmd(), - envCmd(), // DEPRECATED. - genDocsCmd(app), - imgsCmd(), - loginCmd(), - logoutCmd(), - providersCmd(), - resourceCmd(), - satellitesCmd(), - sshCmd(), - syncCmd(), - tagsCmd(), - tokensCmd(), - tunnelCmd(), - updateCmd(), - urlCmd(), - usersCmd(), - workspacesCmd(), - ) - app.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show verbose output") - return app -} - -func genDocsCmd(rootCmd *cobra.Command) *cobra.Command { - return &cobra.Command{ - Use: "gen-docs [dir_path]", - Short: "Generate a markdown documentation tree for the root command.", - Args: xcobra.ExactArgs(1), - Example: "coder gen-docs ./docs", - Hidden: true, - RunE: func(_ *cobra.Command, args []string) error { - return doc.GenMarkdownTree(rootCmd, args[0]) - }, - } -} - -// reference: https://github.com/spf13/cobra/blob/master/shell_completions.md -func completionCmd() *cobra.Command { - return &cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", - Short: "Generate completion script", - Example: `coder completion fish > ~/.config/fish/completions/coder.fish -coder completion zsh > "${fpath[1]}/_coder" - -Linux: - $ coder completion bash > /etc/bash_completion.d/coder -MacOS: - $ coder completion bash > /usr/local/etc/bash_completion.d/coder`, - Long: `To load completions: - -Bash: - -$ source <(coder completion bash) - -To load completions for each session, execute once: -Linux: - $ coder completion bash > /etc/bash_completion.d/coder -MacOS: - $ coder completion bash > /usr/local/etc/bash_completion.d/coder - -Zsh: - -If shell completion is not already enabled in your workspace you will need -to enable it. You can execute the following once: - -$ echo "autoload -U compinit; compinit" >> ~/.zshrc - -To load completions for each session, execute once: -$ coder completion zsh > "${fpath[1]}/_coder" - -You will need to start a new shell for this setup to take effect. - -Fish: - -$ coder completion fish | source - -To load completions for each session, execute once: -$ coder completion fish > ~/.config/fish/completions/coder.fish -`, - DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - Args: cobra.ExactValidArgs(1), - Run: func(cmd *cobra.Command, args []string) { - switch args[0] { - case "bash": - _ = cmd.Root().GenBashCompletion(cmd.OutOrStdout()) // Best effort. - case "zsh": - _ = cmd.Root().GenZshCompletion(cmd.OutOrStdout()) // Best effort. - case "fish": - _ = cmd.Root().GenFishCompletion(cmd.OutOrStdout(), true) // Best effort. - case "powershell": - _ = cmd.Root().GenPowerShellCompletion(cmd.OutOrStdout()) // Best effort. - } - }, - } -} diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go deleted file mode 100644 index a4b9f8de..00000000 --- a/internal/cmd/configssh.go +++ /dev/null @@ -1,314 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "os/user" - "path/filepath" - "runtime" - "sort" - "strings" - - "github.com/cli/safeexec" - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/coderutil" - "cdr.dev/coder-cli/pkg/clog" -) - -const sshStartToken = "# ------------START-CODER-ENTERPRISE-----------" -const sshStartMessage = `# The following has been auto-generated by "coder config-ssh" -# to make accessing your Coder workspaces easier. -# -# To remove this blob, run: -# -# coder config-ssh --remove -# -# You should not hand-edit this section, unless you are deleting it.` -const sshEndToken = "# ------------END-CODER-ENTERPRISE------------" - -func configSSHCmd() *cobra.Command { - var ( - configpath string - remove = false - additionalOptions []string - ) - - cmd := &cobra.Command{ - Use: "config-ssh", - Short: "Configure SSH to access Coder workspaces", - Long: "Inject the proper OpenSSH configuration into your local SSH config file.", - RunE: configSSH(&configpath, &remove, &additionalOptions), - } - cmd.Flags().StringVar(&configpath, "filepath", filepath.Join("~", ".ssh", "config"), "override the default path of your ssh config file") - cmd.Flags().StringSliceVarP(&additionalOptions, "option", "o", []string{}, "additional options injected in the ssh config (ex. disable caching with \"-o ControlPath=none\")") - cmd.Flags().BoolVar(&remove, "remove", false, "remove the auto-generated Coder ssh config") - - return cmd -} - -func configSSH(configpath *string, remove *bool, additionalOptions *[]string) func(cmd *cobra.Command, _ []string) error { - return func(cmd *cobra.Command, _ []string) error { - ctx := cmd.Context() - usr, err := user.Current() - if err != nil { - return xerrors.Errorf("get user home directory: %w", err) - } - - privateKeyFilepath := filepath.Join(usr.HomeDir, ".ssh", "coder_enterprise") - - if strings.HasPrefix(*configpath, "~") { - *configpath = strings.Replace(*configpath, "~", usr.HomeDir, 1) - } - - currentConfig, err := readStr(*configpath) - if os.IsNotExist(err) { - // SSH configs are not always already there. - currentConfig = "" - } else if err != nil { - return xerrors.Errorf("read ssh config file %q: %w", *configpath, err) - } - - currentConfig, didRemoveConfig := removeOldConfig(currentConfig) - if *remove { - if !didRemoveConfig { - return xerrors.Errorf("the Coder ssh configuration section could not be safely deleted or does not exist") - } - - err = writeStr(*configpath, currentConfig) - if err != nil { - return xerrors.Errorf("write to ssh config file %q: %s", *configpath, err) - } - _ = os.Remove(privateKeyFilepath) - - return nil - } - - client, err := newClient(ctx, true) - if err != nil { - return err - } - - user, err := client.Me(ctx) - if err != nil { - return xerrors.Errorf("fetch username: %w", err) - } - - workspaces, err := getWorkspaces(ctx, client, coder.Me) - if err != nil { - return err - } - if len(workspaces) < 1 { - return xerrors.New("no workspaces found") - } - - workspacesWithProviders, err := coderutil.WorkspacesWithProvider(ctx, client, workspaces) - if err != nil { - return xerrors.Errorf("resolve workspace workspace providers: %w", err) - } - - if !sshAvailable(workspacesWithProviders) { - return xerrors.New("SSH is disabled or not available for any workspaces in your Coder deployment.") - } - - binPath, err := binPath() - if err != nil { - return xerrors.Errorf("Failed to get executable path: %w", err) - } - - newConfig := makeNewConfigs(binPath, workspacesWithProviders, privateKeyFilepath, *additionalOptions) - - err = os.MkdirAll(filepath.Dir(*configpath), os.ModePerm) - if err != nil { - return xerrors.Errorf("make configuration directory: %w", err) - } - err = writeStr(*configpath, currentConfig+newConfig) - if err != nil { - return xerrors.Errorf("write new configurations to ssh config file %q: %w", *configpath, err) - } - err = writeSSHKey(ctx, client, privateKeyFilepath) - if err != nil { - if !xerrors.Is(err, os.ErrPermission) { - return xerrors.Errorf("write ssh key: %w", err) - } - fmt.Printf("Your private ssh key already exists at \"%s\"\nYou may need to remove the existing private key file and re-run this command\n\n", privateKeyFilepath) - } else { - fmt.Printf("Your private ssh key was written to \"%s\"\n", privateKeyFilepath) - } - - writeSSHUXState(ctx, client, user.ID, workspaces) - fmt.Printf("An auto-generated ssh config was written to \"%s\"\n", *configpath) - fmt.Println("You should now be able to ssh into your workspace") - fmt.Printf("For example, try running\n\n\t$ ssh coder.%s\n\n", workspaces[0].Name) - return nil - } -} - -// binPath returns the path to the coder binary suitable for use in ssh -// ProxyCommand. -func binPath() (string, error) { - exePath, err := os.Executable() - if err != nil { - return "", xerrors.Errorf("get executable path: %w", err) - } - - // On Windows, the coder-cli executable must be in $PATH for both Msys2/Git - // Bash and OpenSSH for Windows (used by Powershell and VS Code) to function - // correctly. Check if the current executable is in $PATH, and warn the user - // if it isn't. - if runtime.GOOS == goosWindows { - binName := filepath.Base(exePath) - - // We use safeexec instead of os/exec because os/exec returns paths in - // the current working directory, which we will run into very often when - // looking for our own path. - pathPath, err := safeexec.LookPath(binName) - if err != nil { - clog.LogWarn( - "The current executable is not in $PATH.", - "This may lead to problems connecting to your workspace via SSH.", - fmt.Sprintf("Please move %q to a location in your $PATH (such as System32) and run `%s config-ssh` again.", binName, binName), - ) - // Return the exePath so SSH at least works outside of Msys2. - return exePath, nil - } - - // Warn the user if the current executable is not the same as the one in - // $PATH. - if filepath.Clean(pathPath) != filepath.Clean(exePath) { - clog.LogWarn( - "The current executable path does not match the executable path found in $PATH.", - "This may lead to problems connecting to your workspace via SSH.", - fmt.Sprintf("\t Current executable path: %q", exePath), - fmt.Sprintf("\tExecutable path in $PATH: %q", pathPath), - ) - } - - return binName, nil - } - - // On platforms other than Windows we can use the full path to the binary. - return exePath, nil -} - -// removeOldConfig removes the old ssh configuration from the user's sshconfig. -// Returns true if the config was modified. -func removeOldConfig(config string) (string, bool) { - startIndex := strings.Index(config, sshStartToken) - endIndex := strings.Index(config, sshEndToken) - - if startIndex == -1 || endIndex == -1 { - return config, false - } - if startIndex == 0 { - return config[endIndex+len(sshEndToken)+1:], true - } - return config[:startIndex-1] + config[endIndex+len(sshEndToken)+1:], true -} - -// sshAvailable returns true if SSH is available for at least one workspace. -func sshAvailable(workspaces []coderutil.WorkspaceWithWorkspaceProvider) bool { - for _, workspace := range workspaces { - if workspace.WorkspaceProvider.SSHEnabled { - return true - } - } - return false -} - -func writeSSHKey(ctx context.Context, client coder.Client, privateKeyPath string) error { - key, err := client.SSHKey(ctx) - if err != nil { - return err - } - return ioutil.WriteFile(privateKeyPath, []byte(key.PrivateKey), 0600) -} - -func makeNewConfigs(binPath string, workspaces []coderutil.WorkspaceWithWorkspaceProvider, privateKeyFilepath string, additionalOptions []string) string { - newConfig := fmt.Sprintf("\n%s\n%s\n\n", sshStartToken, sshStartMessage) - - sort.Slice(workspaces, func(i, j int) bool { return workspaces[i].Workspace.Name < workspaces[j].Workspace.Name }) - - for _, workspace := range workspaces { - if !workspace.WorkspaceProvider.SSHEnabled { - clog.LogWarn(fmt.Sprintf("SSH is not enabled for workspace provider %q", workspace.WorkspaceProvider.Name), - clog.BlankLine, - clog.Tipf("ask an infrastructure administrator to enable SSH for this workspace provider"), - ) - continue - } - - newConfig += makeSSHConfig(binPath, workspace.Workspace.Name, privateKeyFilepath, additionalOptions) - } - newConfig += fmt.Sprintf("\n%s\n", sshEndToken) - - return newConfig -} - -func makeSSHConfig(binPath, workspaceName, privateKeyFilepath string, additionalOptions []string) string { - // Custom user options come first to maximizessh customization. - options := []string{} - if len(additionalOptions) > 0 { - options = []string{ - "# Custom options. Duplicated values will always prefer the first!", - } - options = append(options, additionalOptions...) - options = append(options, "# End custom options.") - } - options = append(options, - fmt.Sprintf("HostName coder.%s", workspaceName), - fmt.Sprintf("ProxyCommand %s", proxyCommand(binPath, workspaceName, true)), - "StrictHostKeyChecking no", - "ConnectTimeout=0", - "IdentitiesOnly yes", - fmt.Sprintf("IdentityFile=%q", privateKeyFilepath), - ) - - if runtime.GOOS == goosLinux || runtime.GOOS == goosDarwin { - options = append(options, - "ControlMaster auto", - "ControlPath ~/.ssh/.connection-%r@%h:%p", - "ControlPersist 600", - ) - } - - return fmt.Sprintf("Host coder.%s\n\t%s\n\n", workspaceName, strings.Join(options, "\n\t")) -} - -func proxyCommand(binPath, workspaceName string, quoted bool) string { - if quoted { - binPath = fmt.Sprintf("%q", binPath) - } - return fmt.Sprintf(`%s tunnel %s 12213 stdio`, binPath, workspaceName) -} - -func writeStr(filename, data string) error { - return ioutil.WriteFile(filename, []byte(data), 0777) -} - -func readStr(filename string) (string, error) { - contents, err := ioutil.ReadFile(filename) - if err != nil { - return "", err - } - return string(contents), nil -} - -func writeSSHUXState(ctx context.Context, client coder.Client, userID string, workspaces []coder.Workspace) { - // Create a map of workspace.ID -> true to indicate to the web client that all - // current workspaces have SSH configured - cliSSHConfigured := make(map[string]bool) - for _, workspace := range workspaces { - cliSSHConfigured[workspace.ID] = true - } - // Update UXState that coder config-ssh has been run by the currently - // authenticated user - err := client.UpdateUXState(ctx, userID, map[string]interface{}{"cliSSHConfigured": cliSSHConfigured}) - if err != nil { - clog.LogWarn("The Coder web client may not recognize that you've configured SSH.") - } -} diff --git a/internal/cmd/devurls_test.go b/internal/cmd/devurls_test.go deleted file mode 100644 index 3e3c2bd4..00000000 --- a/internal/cmd/devurls_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package cmd - -import ( - "testing" -) - -func Test_devurls(t *testing.T) { - skipIfNoAuth(t) - res := execute(t, nil, "urls", "ls") - res.error(t) -} diff --git a/internal/cmd/errors.go b/internal/cmd/errors.go deleted file mode 100644 index dce13918..00000000 --- a/internal/cmd/errors.go +++ /dev/null @@ -1,71 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/clog" -) - -// handleAPIError attempts to convert an api error into a more detailed clog error. -// If it cannot, it will return the original error. -func handleAPIError(origError error) error { - var httpError *coder.HTTPError - if !xerrors.As(origError, &httpError) { - return origError // Return the original - } - - ae, err := httpError.Payload() - if err != nil { - return origError // Return the original - } - - switch ae.Err.Code { - case "wac_template": // template parse errors - type templatePayload struct { - ErrorType string `json:"error_type"` - Msgs []string `json:"messages"` - } - - var p templatePayload - err := json.Unmarshal(ae.Err.Details, &p) - if err != nil { - return origError - } - - return clog.Error(p.ErrorType, p.Msgs...) - case "verbose": - type verbosePayload struct { - Verbose string `json:"verbose"` - } - var p verbosePayload - err := json.Unmarshal(ae.Err.Details, &p) - if err != nil { - return origError - } - - return clog.Error(origError.Error(), p.Verbose) - case "precondition": - type preconditionPayload struct { - Error string `json:"error"` - Message string `json:"message"` - Solution string `json:"solution"` - } - - var p preconditionPayload - err := json.Unmarshal(ae.Err.Details, &p) - if err != nil { - return origError - } - - return clog.Error(fmt.Sprintf("Precondition Error : Status Code=%d", httpError.StatusCode()), - p.Message, - clog.BlankLine, - clog.Tipf(p.Solution)) - } - - return origError // Return the original -} diff --git a/internal/cmd/images.go b/internal/cmd/images.go deleted file mode 100644 index ccb68ee5..00000000 --- a/internal/cmd/images.go +++ /dev/null @@ -1,88 +0,0 @@ -package cmd - -import ( - "encoding/json" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/clog" - "cdr.dev/coder-cli/pkg/tablewriter" -) - -func imgsCmd() *cobra.Command { - var user string - - cmd := &cobra.Command{ - Use: "images", - Short: "Manage Coder images", - Long: "Manage existing images and/or import new ones.", - } - - cmd.PersistentFlags().StringVar(&user, "user", coder.Me, "Specifies the user by email") - cmd.AddCommand(lsImgsCommand(&user)) - return cmd -} - -func lsImgsCommand(user *string) *cobra.Command { - var ( - orgName string - outputFmt string - ) - - cmd := &cobra.Command{ - Use: "ls", - Short: "list all images available to the active user", - Long: "List all Coder images available to the active user.", - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - client, err := newClient(ctx, true) - if err != nil { - return err - } - - imgs, err := getImgs(ctx, client, - getImgsConf{ - email: *user, - orgName: orgName, - }, - ) - - if err != nil { - return err - } - - if len(imgs) < 1 { - clog.LogInfo("no images found") - imgs = []coder.Image{} // ensures that json output still marshals - } - - switch outputFmt { - case jsonOutput: - enc := json.NewEncoder(cmd.OutOrStdout()) - // pretty print the json - enc.SetIndent("", "\t") - - if err := enc.Encode(imgs); err != nil { - return xerrors.Errorf("write images as JSON: %w", err) - } - return nil - case humanOutput: - err = tablewriter.WriteTable(cmd.OutOrStdout(), len(imgs), func(i int) interface{} { - return imgs[i] - }) - if err != nil { - return xerrors.Errorf("write table: %w", err) - } - return nil - default: - return xerrors.Errorf("%q is not a supported value for --output", outputFmt) - } - }, - } - cmd.Flags().StringVar(&orgName, "org", "", "organization name") - cmd.Flags().StringVar(&outputFmt, "output", humanOutput, "human | json") - return cmd -} diff --git a/internal/cmd/images_test.go b/internal/cmd/images_test.go deleted file mode 100644 index b5823ff6..00000000 --- a/internal/cmd/images_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package cmd - -import ( - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/coder-sdk" -) - -func Test_images(t *testing.T) { - res := execute(t, nil, "images", "--help") - res.success(t) - - res = execute(t, nil, "images", "ls") - res.success(t) - - var images []coder.Image - res = execute(t, nil, "images", "ls", "--output=json") - res.success(t) - res.stdoutUnmarshals(t, &images) - assert.True(t, "more than 0 images", len(images) > 0) - - res = execute(t, nil, "images", "ls", "--org=doesntexist") - res.error(t) - res.stderrContains(t, "org name \"doesntexist\" not found") -} diff --git a/internal/cmd/login.go b/internal/cmd/login.go deleted file mode 100644 index 69d9bfd2..00000000 --- a/internal/cmd/login.go +++ /dev/null @@ -1,152 +0,0 @@ -package cmd - -import ( - "bufio" - "context" - "fmt" - "io/ioutil" - "net/url" - "os/exec" - "runtime" - "strings" - - "github.com/pkg/browser" - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/config" - "cdr.dev/coder-cli/internal/version" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/clog" -) - -func loginCmd() *cobra.Command { - return &cobra.Command{ - Use: "login [Coder URL eg. https://my.coder.domain/]", - Short: "Authenticate this client for future operations", - Args: xcobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - // Pull the URL from the args and do some sanity check. - rawURL := args[0] - if rawURL == "" || !strings.HasPrefix(rawURL, "http") { - return xerrors.Errorf("invalid URL") - } - u, err := url.Parse(rawURL) - if err != nil { - return xerrors.Errorf("parse url: %w", err) - } - // Remove the trailing '/' if any. - u.Path = strings.TrimSuffix(u.Path, "/") - - // From this point, the commandline is correct. - // Don't return errors as it would print the usage. - - if err := login(cmd, u); err != nil { - return xerrors.Errorf("login error: %w", err) - } - return nil - }, - } -} - -// storeConfig writes the workspace URL and session token to the local config directory. -// The config lib will handle the local config path lookup and creation. -func storeConfig(workspaceURL *url.URL, sessionToken string, urlCfg, sessionCfg config.File) error { - if err := urlCfg.Write(workspaceURL.String()); err != nil { - return xerrors.Errorf("store workspace url: %w", err) - } - if err := sessionCfg.Write(sessionToken); err != nil { - return xerrors.Errorf("store session token: %w", err) - } - return nil -} - -func login(cmd *cobra.Command, workspaceURL *url.URL) error { - authURL := *workspaceURL - authURL.Path = workspaceURL.Path + "/internal-auth" - q := authURL.Query() - q.Add("show_token", "true") - authURL.RawQuery = q.Encode() - - if err := openURL(authURL.String()); err != nil { - clog.LogWarn(err.Error()) - fmt.Printf("Open the following in your browser:\n\n\t%s\n\n", authURL.String()) - } else { - fmt.Printf("Your browser has been opened to visit:\n\n\t%s\n\n", authURL.String()) - } - - fmt.Print("Paste token here: ") - var token string - scanner := bufio.NewScanner(cmd.InOrStdin()) - _ = scanner.Scan() - token = scanner.Text() - if err := scanner.Err(); err != nil { - return xerrors.Errorf("reading standard input: %w", err) - } - - if err := pingAPI(cmd.Context(), workspaceURL, token); err != nil { - return xerrors.Errorf("ping API with credentials: %w", err) - } - if err := storeConfig(workspaceURL, token, config.URL, config.Session); err != nil { - return xerrors.Errorf("store auth: %w", err) - } - clog.LogSuccess("logged in") - return nil -} - -// pingAPI creates a client from the given url/token and try to exec an api call. -// Not using the SDK as we want to verify the url/token pair before storing the config files. -func pingAPI(ctx context.Context, workspaceURL *url.URL, token string) error { - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: workspaceURL, - Token: token, - }) - if err != nil { - return xerrors.Errorf("failed to create coder.Client: %w", err) - } - - if apiVersion, err := client.APIVersion(ctx); err == nil { - if apiVersion != "" && !version.VersionsMatch(apiVersion) { - logVersionMismatchError(apiVersion) - } - } - _, err = client.Me(ctx) - if err != nil { - return xerrors.Errorf("call api: %w", err) - } - return nil -} - -// isWSL determines if coder-cli is running within Windows Subsystem for Linux. -func isWSL() (bool, error) { - if runtime.GOOS == goosDarwin || runtime.GOOS == goosWindows { - return false, nil - } - data, err := ioutil.ReadFile("/proc/version") - if err != nil { - return false, xerrors.Errorf("read /proc/version: %w", err) - } - return strings.Contains(strings.ToLower(string(data)), "microsoft"), nil -} - -// openURL opens the provided URL via user's default browser. -func openURL(url string) error { - var cmd string - var args []string - - wsl, err := isWSL() - if err != nil { - return xerrors.Errorf("test running Windows Subsystem for Linux: %w", err) - } - - if wsl { - cmd = "cmd.exe" - args = []string{"/c", "start"} - url = strings.ReplaceAll(url, "&", "^&") - args = append(args, url) - return exec.Command(cmd, args...).Start() - } - - return browser.OpenURL(url) -} diff --git a/internal/cmd/logout.go b/internal/cmd/logout.go deleted file mode 100644 index fd864aa1..00000000 --- a/internal/cmd/logout.go +++ /dev/null @@ -1,32 +0,0 @@ -package cmd - -import ( - "os" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/internal/config" - "cdr.dev/coder-cli/pkg/clog" -) - -func logoutCmd() *cobra.Command { - return &cobra.Command{ - Use: "logout", - Short: "Remove local authentication credentials if any exist", - RunE: logout, - } -} - -func logout(_ *cobra.Command, _ []string) error { - err := config.Session.Delete() - if err != nil { - if os.IsNotExist(err) { - clog.LogInfo("no active session") - return nil - } - return xerrors.Errorf("delete session: %w", err) - } - clog.LogSuccess("logged out") - return nil -} diff --git a/internal/cmd/providers.go b/internal/cmd/providers.go deleted file mode 100644 index 5081df2b..00000000 --- a/internal/cmd/providers.go +++ /dev/null @@ -1,319 +0,0 @@ -package cmd - -import ( - "fmt" - "net/url" - - "cdr.dev/coder-cli/internal/coderutil" - "cdr.dev/coder-cli/internal/x/xcobra" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/clog" - "cdr.dev/coder-cli/pkg/tablewriter" -) - -func providersCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "providers", - Short: "Interact with Coder workspace providers", - Long: "Perform operations on the Coder Workspace Providers for the platform.", - Hidden: true, - } - - cmd.AddCommand( - createProviderCmd(), - listProviderCmd(), - deleteProviderCmd(), - cordonProviderCmd(), - unCordonProviderCmd(), - renameProviderCmd(), - ) - return cmd -} - -func createProviderCmd() *cobra.Command { - var ( - hostname string - clusterAddress string - ) - cmd := &cobra.Command{ - Use: "create [name] --hostname=[hostname] --cluster-address=[clusterAddress]", - Args: xcobra.ExactArgs(1), - Short: "create a new workspace provider.", - Long: "Create a new Coder workspace provider.", - Example: `# create a new workspace provider in a pending state - -coder providers create my-provider --hostname=https://provider.example.com --cluster-address=https://255.255.255.255`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - client, err := newClient(ctx, true) - if err != nil { - return err - } - - version, err := client.APIVersion(ctx) - if err != nil { - return xerrors.Errorf("get application version: %w", err) - } - - cemanagerURL := client.BaseURL() - ingressHost, err := url.Parse(hostname) - if err != nil { - return xerrors.Errorf("parse hostname: %w", err) - } - - if cemanagerURL.Scheme != ingressHost.Scheme { - return xerrors.Errorf("Coder access url and hostname must have matching protocols: coder access url: %s, workspace provider hostname: %s", cemanagerURL.String(), ingressHost.String()) - } - - // ExactArgs(1) ensures our name value can't panic on an out of bounds. - createReq := &coder.CreateWorkspaceProviderReq{ - Name: args[0], - Type: coder.WorkspaceProviderKubernetes, - Hostname: hostname, - ClusterAddress: clusterAddress, - } - - wp, err := client.CreateWorkspaceProvider(ctx, *createReq) - if err != nil { - return xerrors.Errorf("create workspace provider: %w", err) - } - - var sslNote string - if ingressHost.Scheme == "https" { - sslNote = ` -NOTE: Since the hostname provided is using https you must ensure the deployment -has a valid SSL certificate. See https://coder.com/docs/guides/ssl-certificates -for more information.` - } - - clog.LogSuccess(fmt.Sprintf(` -Created workspace provider "%s" -`, createReq.Name)) - _ = tablewriter.WriteTable(cmd.OutOrStdout(), 1, func(i int) interface{} { - return *wp - }) - _, _ = fmt.Fprint(cmd.OutOrStdout(), ` -Now that the workspace provider is provisioned, it must be deployed into the cluster. To learn more, -visit https://coder.com/docs/workspace-providers/deployment - -When connected to the cluster you wish to deploy onto, use the following helm command: - -helm upgrade coder-workspace-provider coder/workspace-provider \ - --version=`+version+` \ - --atomic \ - --install \ - --force \ - --set envproxy.token=`+wp.EnvproxyToken+` \ - --set envproxy.accessURL=`+ingressHost.String()+` \ - --set ingress.host=`+ingressHost.Hostname()+` \ - --set envproxy.clusterAddress=`+clusterAddress+` \ - --set cemanager.accessURL=`+cemanagerURL.String()+` -`+sslNote+` - -WARNING: The 'envproxy.token' is a secret value that authenticates the workspace provider, -make sure not to share this token or make it public. - -Other values can be set on the helm chart to further customize the deployment, see -https://github.com/cdr/enterprise-helm/blob/workspace-providers-envproxy-only/README.md -`) - - return nil - }, - } - - cmd.Flags().StringVar(&hostname, "hostname", "", "workspace provider hostname") - cmd.Flags().StringVar(&clusterAddress, "cluster-address", "", "kubernetes cluster apiserver endpoint") - _ = cmd.MarkFlagRequired("hostname") - _ = cmd.MarkFlagRequired("cluster-address") - return cmd -} - -func listProviderCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "ls", - Short: "list workspace providers.", - Long: "List all Coder workspace providers.", - Example: `# list workspace providers -coder providers ls`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - client, err := newClient(ctx, true) - if err != nil { - return err - } - - wps, err := client.WorkspaceProviders(ctx) - if err != nil { - return xerrors.Errorf("list workspace providers: %w", err) - } - - err = tablewriter.WriteTable(cmd.OutOrStdout(), len(wps.Kubernetes), func(i int) interface{} { - return wps.Kubernetes[i] - }) - if err != nil { - return xerrors.Errorf("write table: %w", err) - } - return nil - }, - } - return cmd -} - -func deleteProviderCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "rm [workspace_provider_name]", - Short: "remove a workspace provider.", - Long: "Remove an existing Coder workspace provider by name.", - Example: `# remove an existing workspace provider by name -coder providers rm my-workspace-provider`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - - wps, err := client.WorkspaceProviders(ctx) - if err != nil { - return xerrors.Errorf("listing workspace providers: %w", err) - } - - egroup := clog.LoggedErrGroup() - for _, wpName := range args { - name := wpName - egroup.Go(func() error { - var id string - for _, wp := range wps.Kubernetes { - if wp.Name == name { - id = wp.ID - } - } - if id == "" { - return clog.Error( - fmt.Sprintf(`failed to remove workspace provider "%s"`, name), - clog.Causef(`no workspace provider found by name "%s"`, name), - ) - } - - err = client.DeleteWorkspaceProviderByID(ctx, id) - if err != nil { - return clog.Error( - fmt.Sprintf(`failed to remove workspace provider "%s"`, name), - clog.Causef(err.Error()), - ) - } - - clog.LogSuccess(fmt.Sprintf(`removed workspace provider with name "%s"`, name)) - - return nil - }) - } - return egroup.Wait() - }, - } - return cmd -} - -func cordonProviderCmd() *cobra.Command { - var reason string - - cmd := &cobra.Command{ - Use: "cordon [workspace_provider_name]", - Args: xcobra.ExactArgs(1), - Short: "cordon a workspace provider.", - Long: "Prevent an existing Coder workspace provider from supporting any additional workspaces.", - Example: `# cordon an existing workspace provider by name -coder providers cordon my-workspace-provider --reason "limit cloud clost"`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - - wpName := args[0] - provider, err := coderutil.ProviderByName(ctx, client, wpName) - if err != nil { - return err - } - - if err := client.CordonWorkspaceProvider(ctx, provider.ID, reason); err != nil { - return err - } - clog.LogSuccess(fmt.Sprintf("provider %q successfully cordoned - you can no longer create workspaces on this provider without uncordoning first", wpName)) - return nil - }, - } - cmd.Flags().StringVar(&reason, "reason", "", "reason for cordoning the provider") - _ = cmd.MarkFlagRequired("reason") - return cmd -} - -func unCordonProviderCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "uncordon [workspace_provider_name]", - Args: xcobra.ExactArgs(1), - Short: "uncordon a workspace provider.", - Long: "Set a currently cordoned provider as ready; enabling it to continue provisioning resources for new workspaces.", - Example: `# uncordon an existing workspace provider by name -coder providers uncordon my-workspace-provider`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - - wpName := args[0] - provider, err := coderutil.ProviderByName(ctx, client, wpName) - if err != nil { - return err - } - - if err := client.UnCordonWorkspaceProvider(ctx, provider.ID); err != nil { - return err - } - clog.LogSuccess(fmt.Sprintf("provider %q successfully uncordoned - you can now create workspaces on this provider", wpName)) - return nil - }, - } - return cmd -} - -func renameProviderCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "rename [old_name] [new_name]", - Args: xcobra.ExactArgs(2), - Short: "rename a workspace provider.", - Long: "Changes the name field of an existing workspace provider.", - Example: `# rename a workspace provider from 'built-in' to 'us-east-1' -coder providers rename build-in us-east-1`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - - oldName := args[0] - newName := args[1] - provider, err := coderutil.ProviderByName(ctx, client, oldName) - if err != nil { - return err - } - - if err := client.RenameWorkspaceProvider(ctx, provider.ID, newName); err != nil { - return err - } - clog.LogSuccess(fmt.Sprintf("provider %s successfully renamed to %s", oldName, newName)) - return nil - }, - } - return cmd -} diff --git a/internal/cmd/providers_test.go b/internal/cmd/providers_test.go deleted file mode 100644 index 685e129b..00000000 --- a/internal/cmd/providers_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package cmd - -import ( - "testing" -) - -func Test_providers_ls(t *testing.T) { - skipIfNoAuth(t) - res := execute(t, nil, "providers", "ls") - res.success(t) -} diff --git a/internal/cmd/rebuild.go b/internal/cmd/rebuild.go deleted file mode 100644 index 80a4521c..00000000 --- a/internal/cmd/rebuild.go +++ /dev/null @@ -1,186 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/briandowns/spinner" - "github.com/fatih/color" - "github.com/manifoldco/promptui" - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/clog" -) - -func rebuildWorkspaceCommand() *cobra.Command { - var follow bool - var force bool - var user string - cmd := &cobra.Command{ - Use: "rebuild [workspace_name]", - Short: "rebuild a Coder workspace", - Args: xcobra.ExactArgs(1), - Example: `coder workspaces rebuild front-end-workspace --follow -coder workspaces rebuild backend-workspace --force`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - workspace, err := findWorkspace(ctx, client, args[0], user) - if err != nil { - return err - } - - if !force && workspace.LatestStat.ContainerStatus == coder.WorkspaceOn { - _, err = (&promptui.Prompt{ - Label: fmt.Sprintf("Rebuild workspace %q? (will destroy any work outside of your home directory)", workspace.Name), - IsConfirm: true, - }).Run() - if err != nil { - return clog.Fatal( - "failed to confirm prompt", clog.BlankLine, - clog.Tipf(`use "--force" to rebuild without a confirmation prompt`), - ) - } - } - - if err = client.RebuildWorkspace(ctx, workspace.ID); err != nil { - return err - } - if follow { - if err = trailBuildLogs(ctx, client, workspace.ID); err != nil { - return err - } - } else { - clog.LogSuccess( - "successfully started rebuild", - clog.Tipf("run \"coder workspaces watch-build %s\" to follow the build logs", workspace.Name), - ) - } - return nil - }, - } - - cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") - cmd.Flags().BoolVar(&follow, "follow", false, "follow build log after initiating rebuild") - cmd.Flags().BoolVar(&force, "force", false, "force rebuild without showing a confirmation prompt") - return cmd -} - -// trailBuildLogs follows the build log for a given workspace and prints the staged -// output with loaders and success/failure indicators for each stage. -func trailBuildLogs(ctx context.Context, client coder.Client, workspaceID string) error { - const check = "✅" - const failure = "❌" - - newSpinner := func() *spinner.Spinner { return spinner.New(spinner.CharSets[11], 100*time.Millisecond) } - - // this tells us whether to show dynamic loaders when printing output - isTerminal := showInteractiveOutput - - logs, err := client.FollowWorkspaceBuildLog(ctx, workspaceID) - if err != nil { - return err - } - - var s *spinner.Spinner - for l := range logs { - if l.Err != nil { - return l.Err - } - - logTime := l.BuildLog.Time.Local() - msg := fmt.Sprintf("%s %s", logTime.Format(time.RFC3339), l.BuildLog.Msg) - - switch l.BuildLog.Type { - case coder.BuildLogTypeStart: - // the FE uses this to reset the UI - // the CLI doesn't need to do anything here given that we only append to the trail - - case coder.BuildLogTypeStage: - if !isTerminal { - fmt.Println(msg) - continue - } - - if s != nil { - s.Stop() - fmt.Print("\n") - } - - s = newSpinner() - s.Suffix = fmt.Sprintf(" -- %s", msg) - s.FinalMSG = fmt.Sprintf("%s -- %s", check, msg) - s.Start() - - case coder.BuildLogTypeSubstage: - // TODO(@f0ssel) add verbose substage printing - if !verbose { - continue - } - - case coder.BuildLogTypeError: - if !isTerminal { - fmt.Println(msg) - continue - } - - if s != nil { - s.FinalMSG = fmt.Sprintf("%s %s", failure, strings.TrimPrefix(s.Suffix, " ")) - s.Stop() - fmt.Print("\n") - } - - s = newSpinner() - s.Suffix = color.RedString(" -- %s", msg) - s.FinalMSG = color.RedString("%s -- %s", failure, msg) - s.Start() - - case coder.BuildLogTypeDone: - if s != nil { - s.Stop() - fmt.Print("\n") - } - - return nil - default: - return xerrors.Errorf("unknown buildlog type: %s", l.BuildLog.Type) - } - } - return nil -} - -func watchBuildLogCommand() *cobra.Command { - var user string - cmd := &cobra.Command{ - Use: "watch-build [workspace_name]", - Example: "coder workspaces watch-build front-end-workspace", - Short: "trail the build log of a Coder workspace", - Args: xcobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - workspace, err := findWorkspace(ctx, client, args[0], user) - if err != nil { - return err - } - - if err = trailBuildLogs(ctx, client, workspace.ID); err != nil { - return err - } - return nil - }, - } - cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") - return cmd -} diff --git a/internal/cmd/resourcemanager.go b/internal/cmd/resourcemanager.go deleted file mode 100644 index 5be08d24..00000000 --- a/internal/cmd/resourcemanager.go +++ /dev/null @@ -1,433 +0,0 @@ -package cmd - -import ( - "fmt" - "io" - "sort" - "strings" - "text/tabwriter" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/coderutil" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/clog" -) - -func resourceCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "resources", - Short: "manage Coder resources with platform-level context (users, organizations, workspaces)", - Hidden: true, - } - cmd.AddCommand(resourceTop()) - return cmd -} - -type resourceTopOptions struct { - group string - user string - org string - sortBy string - provider string - showEmptyGroups bool -} - -func resourceTop() *cobra.Command { - var options resourceTopOptions - - cmd := &cobra.Command{ - Use: "top", - Short: "resource viewer with Coder platform annotations", - RunE: runResourceTop(&options), - Args: xcobra.ExactArgs(0), - Example: `coder resources top --group org -coder resources top --group org --verbose --org DevOps -coder resources top --group user --verbose --user name@example.com -coder resources top --group provider --verbose --provider myprovider -coder resources top --sort-by memory --show-empty`, - } - cmd.Flags().StringVar(&options.group, "group", "user", "the grouping parameter (user|org|provider)") - cmd.Flags().StringVar(&options.user, "user", "", "filter by a user email") - cmd.Flags().StringVar(&options.org, "org", "", "filter by the name of an organization") - cmd.Flags().StringVar(&options.provider, "provider", "", "filter by the name of a workspace provider") - cmd.Flags().StringVar(&options.sortBy, "sort-by", "cpu", "field to sort aggregate groups and workspaces by (cpu|memory)") - cmd.Flags().BoolVar(&options.showEmptyGroups, "show-empty", false, "show groups with zero active workspaces") - - return cmd -} - -func runResourceTop(options *resourceTopOptions) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - - // NOTE: it's not worth parrallelizing these calls yet given that this specific endpoint - // takes about 20x times longer than the other two - allWorkspaces, err := client.Workspaces(ctx) - if err != nil { - return xerrors.Errorf("get workspaces %w", err) - } - // only include workspaces whose last status was "ON" - workspaces := make([]coder.Workspace, 0) - for _, e := range allWorkspaces { - if e.LatestStat.ContainerStatus == coder.WorkspaceOn { - workspaces = append(workspaces, e) - } - } - - users, err := client.Users(ctx) - if err != nil { - return xerrors.Errorf("get users: %w", err) - } - images, err := coderutil.MakeImageMap(ctx, client, workspaces) - if err != nil { - return xerrors.Errorf("get images: %w", err) - } - - orgs, err := client.Organizations(ctx) - if err != nil { - return xerrors.Errorf("get organizations: %w", err) - } - - providers, err := client.WorkspaceProviders(ctx) - if err != nil { - return xerrors.Errorf("get workspace providers: %w", err) - } - data := entities{ - providers: providers.Kubernetes, - users: users, - orgs: orgs, - workspaces: workspaces, - images: images, - } - return presentEntites(cmd.OutOrStdout(), data, *options) - } -} - -func presentEntites(w io.Writer, data entities, options resourceTopOptions) error { - var ( - groups []groupable - labeler workspaceLabeler - ) - switch options.group { - case "user": - groups, labeler = aggregateByUser(data, options) - case "org": - groups, labeler = aggregateByOrg(data, options) - case "provider": - groups, labeler = aggregateByProvider(data, options) - default: - return xerrors.Errorf("unknown --group %q", options.group) - } - - return printResourceTop(w, groups, labeler, options.showEmptyGroups, options.sortBy) -} - -type entities struct { - providers []coder.KubernetesProvider - users []coder.User - orgs []coder.Organization - workspaces []coder.Workspace - images map[string]*coder.Image -} - -func aggregateByUser(data entities, options resourceTopOptions) ([]groupable, workspaceLabeler) { - var groups []groupable - providerIDMap := providerIDs(data.providers) - orgIDMap := make(map[string]coder.Organization) - for _, o := range data.orgs { - orgIDMap[o.ID] = o - } - userWorkspaces := make(map[string][]coder.Workspace, len(data.users)) - for _, e := range data.workspaces { - if options.org != "" && orgIDMap[e.OrganizationID].Name != options.org { - continue - } - userWorkspaces[e.UserID] = append(userWorkspaces[e.UserID], e) - } - for _, u := range data.users { - if options.user != "" && u.Email != options.user { - continue - } - groups = append(groups, userGrouping{user: u, userWorkspaces: userWorkspaces[u.ID]}) - } - return groups, labelAll(imgLabeler(data.images), providerLabeler(providerIDMap), orgLabeler(orgIDMap)) -} - -func userIDs(users []coder.User) map[string]coder.User { - userIDMap := make(map[string]coder.User) - for _, u := range users { - userIDMap[u.ID] = u - } - return userIDMap -} - -func aggregateByOrg(data entities, options resourceTopOptions) ([]groupable, workspaceLabeler) { - var groups []groupable - providerIDMap := providerIDs(data.providers) - orgWorkspaces := make(map[string][]coder.Workspace, len(data.orgs)) - userIDMap := userIDs(data.users) - for _, e := range data.workspaces { - if options.user != "" && userIDMap[e.UserID].Email != options.user { - continue - } - orgWorkspaces[e.OrganizationID] = append(orgWorkspaces[e.OrganizationID], e) - } - for _, o := range data.orgs { - if options.org != "" && o.Name != options.org { - continue - } - groups = append(groups, orgGrouping{org: o, orgWorkspaces: orgWorkspaces[o.ID]}) - } - return groups, labelAll(imgLabeler(data.images), userLabeler(userIDMap), providerLabeler(providerIDMap)) -} - -func providerIDs(providers []coder.KubernetesProvider) map[string]coder.KubernetesProvider { - providerIDMap := make(map[string]coder.KubernetesProvider) - for _, p := range providers { - providerIDMap[p.ID] = p - } - return providerIDMap -} - -func aggregateByProvider(data entities, options resourceTopOptions) ([]groupable, workspaceLabeler) { - var groups []groupable - providerIDMap := providerIDs(data.providers) - userIDMap := userIDs(data.users) - providerWorkspaces := make(map[string][]coder.Workspace, len(data.providers)) - for _, e := range data.workspaces { - if options.provider != "" && providerIDMap[e.ResourcePoolID].Name != options.provider { - continue - } - providerWorkspaces[e.ResourcePoolID] = append(providerWorkspaces[e.ResourcePoolID], e) - } - for _, p := range data.providers { - if options.provider != "" && p.Name != options.provider { - continue - } - groups = append(groups, providerGrouping{provider: p, providerWorkspaces: providerWorkspaces[p.ID]}) - } - return groups, labelAll(imgLabeler(data.images), userLabeler(userIDMap)) // TODO: consider adding an org label here -} - -// groupable specifies a structure capable of being an aggregation group of workspaces (user, org, all). -type groupable interface { - header() string - workspaces() []coder.Workspace -} - -type userGrouping struct { - user coder.User - userWorkspaces []coder.Workspace -} - -func (u userGrouping) workspaces() []coder.Workspace { - return u.userWorkspaces -} - -func (u userGrouping) header() string { - return fmt.Sprintf("%s\t(%s)", truncate(u.user.Name, 20, "..."), u.user.Email) -} - -type orgGrouping struct { - org coder.Organization - orgWorkspaces []coder.Workspace -} - -func (o orgGrouping) workspaces() []coder.Workspace { - return o.orgWorkspaces -} - -func (o orgGrouping) header() string { - plural := "s" - if len(o.org.Members) == 1 { - plural = "" - } - return fmt.Sprintf("%s\t(%v member%s)", truncate(o.org.Name, 20, "..."), len(o.org.Members), plural) -} - -type providerGrouping struct { - provider coder.KubernetesProvider - providerWorkspaces []coder.Workspace -} - -func (p providerGrouping) workspaces() []coder.Workspace { - return p.providerWorkspaces -} - -func (p providerGrouping) header() string { - return fmt.Sprintf("%s\t", truncate(p.provider.Name, 20, "...")) -} - -func printResourceTop(writer io.Writer, groups []groupable, labeler workspaceLabeler, showEmptyGroups bool, sortBy string) error { - tabwriter := tabwriter.NewWriter(writer, 0, 0, 4, ' ', 0) - defer func() { _ = tabwriter.Flush() }() - - var userResources []aggregatedResources - for _, group := range groups { - if !showEmptyGroups && len(group.workspaces()) < 1 { - continue - } - userResources = append(userResources, aggregatedResources{ - groupable: group, resources: aggregateWorkspaceResources(group.workspaces()), - }) - } - - err := sortAggregatedResources(userResources, sortBy) - if err != nil { - return err - } - - for _, u := range userResources { - _, _ = fmt.Fprintf(tabwriter, "%s\t%s", u.header(), u.resources) - if verbose { - if len(u.workspaces()) > 0 { - _, _ = fmt.Fprintf(tabwriter, "\f") - } - for _, workspace := range u.workspaces() { - _, _ = fmt.Fprintf(tabwriter, "\t") - _, _ = fmt.Fprintln(tabwriter, fmtWorkspaceResources(workspace, labeler)) - } - } - _, _ = fmt.Fprint(tabwriter, "\n") - } - if len(userResources) == 0 { - clog.LogInfo( - "no groups for the given filters exist with active workspaces", - clog.Tipf("run \"--show-empty\" to see groups with no resources."), - ) - } - return nil -} - -func sortAggregatedResources(resources []aggregatedResources, sortBy string) error { - const cpu = "cpu" - const memory = "memory" - switch sortBy { - case cpu: - sort.Slice(resources, func(i, j int) bool { - return resources[i].cpuAllocation > resources[j].cpuAllocation - }) - case memory: - sort.Slice(resources, func(i, j int) bool { - return resources[i].memAllocation > resources[j].memAllocation - }) - default: - return xerrors.Errorf("unknown --sort-by value of \"%s\"", sortBy) - } - for _, group := range resources { - workspaces := group.workspaces() - switch sortBy { - case cpu: - sort.Slice(workspaces, func(i, j int) bool { return workspaces[i].CPUCores > workspaces[j].CPUCores }) - case memory: - sort.Slice(workspaces, func(i, j int) bool { return workspaces[i].MemoryGB > workspaces[j].MemoryGB }) - default: - return xerrors.Errorf("unknown --sort-by value of \"%s\"", sortBy) - } - } - return nil -} - -type aggregatedResources struct { - groupable - resources -} - -func resourcesFromWorkspace(workspace coder.Workspace) resources { - return resources{ - cpuAllocation: workspace.CPUCores, - cpuUtilization: workspace.LatestStat.CPUUsage, - memAllocation: workspace.MemoryGB, - memUtilization: workspace.LatestStat.MemoryUsage, - } -} - -func fmtWorkspaceResources(workspace coder.Workspace, labeler workspaceLabeler) string { - return fmt.Sprintf("%s\t%s\t%s", truncate(workspace.Name, 20, "..."), resourcesFromWorkspace(workspace), labeler.label(workspace)) -} - -type workspaceLabeler interface { - label(coder.Workspace) string -} - -func labelAll(labels ...workspaceLabeler) workspaceLabeler { return multiLabeler(labels) } - -type multiLabeler []workspaceLabeler - -func (m multiLabeler) label(e coder.Workspace) string { - var str strings.Builder - for i, labeler := range m { - if i != 0 { - str.WriteString("\t") - } - str.WriteString(labeler.label(e)) - } - return str.String() -} - -type orgLabeler map[string]coder.Organization - -func (o orgLabeler) label(e coder.Workspace) string { - return fmt.Sprintf("[org: %s]", o[e.OrganizationID].Name) -} - -type imgLabeler map[string]*coder.Image - -func (i imgLabeler) label(e coder.Workspace) string { - return fmt.Sprintf("[img: %s:%s]", i[e.ImageID].Repository, e.ImageTag) -} - -type userLabeler map[string]coder.User - -func (u userLabeler) label(e coder.Workspace) string { - return fmt.Sprintf("[user: %s]", u[e.UserID].Email) -} - -type providerLabeler map[string]coder.KubernetesProvider - -func (p providerLabeler) label(e coder.Workspace) string { - return fmt.Sprintf("[provider: %s]", p[e.ResourcePoolID].Name) -} - -func aggregateWorkspaceResources(workspaces []coder.Workspace) resources { - var aggregate resources - for _, e := range workspaces { - aggregate.cpuAllocation += e.CPUCores - aggregate.cpuUtilization += e.LatestStat.CPUUsage - aggregate.memAllocation += e.MemoryGB - aggregate.memUtilization += e.LatestStat.MemoryUsage - } - return aggregate -} - -type resources struct { - cpuAllocation float32 - memAllocation float32 - - // TODO: consider using these - cpuUtilization float32 - memUtilization float32 -} - -func (a resources) String() string { - return fmt.Sprintf( - "[cpu: %.1f]\t[mem: %.1f GB]", - a.cpuAllocation, a.memAllocation, - ) -} - -//nolint:unparam -// truncate the given string and replace the removed chars with some replacement (ex: "..."). -func truncate(str string, max int, replace string) string { - if len(str) <= max { - return str - } - return str[:max+1] + replace -} diff --git a/internal/cmd/resourcemanager_test.go b/internal/cmd/resourcemanager_test.go deleted file mode 100644 index ffe3dbb9..00000000 --- a/internal/cmd/resourcemanager_test.go +++ /dev/null @@ -1,184 +0,0 @@ -package cmd - -import ( - "bytes" - "flag" - "fmt" - "io/ioutil" - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/coder-sdk" -) - -var write = flag.Bool("write", false, "write to the golden files") - -func Test_resourceManager(t *testing.T) { - // TODO: cleanup - verbose = true - - const goldenFile = "resourcemanager_test.golden" - var buff bytes.Buffer - data := mockResourceTopEntities() - tests := []struct { - header string - data entities - options resourceTopOptions - }{ - { - header: "By User", - data: data, - options: resourceTopOptions{ - group: "user", - sortBy: "cpu", - }, - }, - { - header: "By Org", - data: data, - options: resourceTopOptions{ - group: "org", - sortBy: "cpu", - }, - }, - { - header: "By Provider", - data: data, - options: resourceTopOptions{ - group: "provider", - sortBy: "cpu", - }, - }, - { - header: "Sort By Memory", - data: data, - options: resourceTopOptions{ - group: "user", - sortBy: "memory", - }, - }, - } - - for _, tcase := range tests { - buff.WriteString(fmt.Sprintf("=== TEST: %s\n", tcase.header)) - err := presentEntites(&buff, tcase.data, tcase.options) - assert.Success(t, "present entities", err) - } - - assertGolden(t, goldenFile, buff.Bytes()) -} - -func assertGolden(t *testing.T, path string, output []byte) { - if *write { - err := ioutil.WriteFile(path, output, 0777) - assert.Success(t, "write file", err) - return - } - goldenContent, err := ioutil.ReadFile(path) - assert.Success(t, "read golden file", err) - assert.Equal(t, "golden content matches", string(goldenContent), string(output)) -} - -func mockResourceTopEntities() entities { - orgIDs := [...]string{randString(10), randString(10), randString(10)} - imageIDs := [...]string{randString(10), randString(10), randString(10)} - providerIDs := [...]string{randString(10), randString(10), randString(10)} - userIDs := [...]string{randString(10), randString(10), randString(10)} - workspaceIDs := [...]string{randString(10), randString(10), randString(10), randString(10)} - - return entities{ - providers: []coder.KubernetesProvider{ - { - ID: providerIDs[0], - Name: "mars", - }, - { - ID: providerIDs[1], - Name: "underground", - }, - }, - users: []coder.User{ - { - ID: userIDs[0], - Name: "Random", - Email: "random@coder.com", - }, - { - ID: userIDs[1], - Name: "Second Random", - Email: "second-random@coder.com", - }, - }, - orgs: []coder.Organization{ - { - ID: orgIDs[0], - Name: "SpecialOrg", - - //! these should probably be fixed, but for now they are just for the count - Members: []coder.OrganizationUser{{}, {}}, - }, - { - ID: orgIDs[1], - Name: "NotSoSpecialOrg", - - //! these should probably be fixed, but for now they are just for the count - Members: []coder.OrganizationUser{{}, {}}, - }, - }, - workspaces: []coder.Workspace{ - { - ID: workspaceIDs[0], - ResourcePoolID: providerIDs[0], - ImageID: imageIDs[0], - OrganizationID: orgIDs[0], - UserID: userIDs[0], - Name: "dev-workspace", - ImageTag: "20.04", - CPUCores: 12.2, - MemoryGB: 64.4, - LatestStat: coder.WorkspaceStat{ - ContainerStatus: coder.WorkspaceOn, - }, - }, - { - ID: workspaceIDs[1], - ResourcePoolID: providerIDs[1], - ImageID: imageIDs[1], - OrganizationID: orgIDs[1], - UserID: userIDs[1], - Name: "another-workspace", - ImageTag: "10.2", - CPUCores: 4, - MemoryGB: 16, - LatestStat: coder.WorkspaceStat{ - ContainerStatus: coder.WorkspaceOn, - }, - }, - { - ID: workspaceIDs[2], - ResourcePoolID: providerIDs[1], - ImageID: imageIDs[1], - OrganizationID: orgIDs[1], - UserID: userIDs[1], - Name: "yet-another-workspace", - ImageTag: "10.2", - CPUCores: 100, - MemoryGB: 2, - LatestStat: coder.WorkspaceStat{ - ContainerStatus: coder.WorkspaceOn, - }, - }, - }, - images: map[string]*coder.Image{ - imageIDs[0]: { - Repository: "ubuntu", - OrganizationID: orgIDs[0], - }, - imageIDs[1]: { - Repository: "archlinux", - OrganizationID: orgIDs[0], - }, - }, - } -} diff --git a/internal/cmd/resourcemanager_test.golden b/internal/cmd/resourcemanager_test.golden deleted file mode 100755 index 0bed13ee..00000000 --- a/internal/cmd/resourcemanager_test.golden +++ /dev/null @@ -1,32 +0,0 @@ -=== TEST: By User -Second Random (second-random@coder.com) [cpu: 104.0] [mem: 18.0 GB] - yet-another-workspace... [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] - another-workspace [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] - -Random (random@coder.com) [cpu: 12.2] [mem: 64.4 GB] - dev-workspace [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [provider: mars] [org: SpecialOrg] - -=== TEST: By Org -NotSoSpecialOrg (2 members) [cpu: 104.0] [mem: 18.0 GB] - yet-another-workspace... [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] [provider: underground] - another-workspace [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] [provider: underground] - -SpecialOrg (2 members) [cpu: 12.2] [mem: 64.4 GB] - dev-workspace [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [user: random@coder.com] [provider: mars] - -=== TEST: By Provider -underground [cpu: 104.0] [mem: 18.0 GB] - yet-another-workspace... [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] - another-workspace [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] - -mars [cpu: 12.2] [mem: 64.4 GB] - dev-workspace [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [user: random@coder.com] - -=== TEST: Sort By Memory -Random (random@coder.com) [cpu: 12.2] [mem: 64.4 GB] - dev-workspace [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [provider: mars] [org: SpecialOrg] - -Second Random (second-random@coder.com) [cpu: 104.0] [mem: 18.0 GB] - another-workspace [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] - yet-another-workspace... [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] - diff --git a/internal/cmd/satellites.go b/internal/cmd/satellites.go deleted file mode 100644 index 982451f7..00000000 --- a/internal/cmd/satellites.go +++ /dev/null @@ -1,222 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "strings" - - "cdr.dev/coder-cli/internal/x/xcobra" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/clog" - "cdr.dev/coder-cli/pkg/tablewriter" -) - -const ( - satelliteKeyPath = "/api/private/satellites/key" -) - -type satelliteKeyResponse struct { - Key string `json:"key"` - Fingerprint string `json:"fingerprint"` -} - -func satellitesCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "satellites", - Short: "Interact with Coder satellite deployments", - Long: "Perform operations on the Coder satellites for the platform.", - } - - cmd.AddCommand( - createSatelliteCmd(), - listSatellitesCmd(), - deleteSatelliteCmd(), - ) - return cmd -} - -func createSatelliteCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "create [name] [satellite_access_url]", - Args: xcobra.ExactArgs(2), - Short: "create a new satellite.", - Long: "Create a new Coder satellite.", - Example: `# create a new satellite - -coder satellites create eu-west https://eu-west.coder.com`, - RunE: func(cmd *cobra.Command, args []string) error { - var ( - ctx = cmd.Context() - name = args[0] - accessURL = args[1] - ) - - client, err := newClient(ctx, true) - if err != nil { - return xerrors.Errorf("making coder client: %w", err) - } - - sURL, err := url.Parse(accessURL) - if err != nil { - return xerrors.Errorf("parsing satellite access url: %w", err) - } - sURL.Path = satelliteKeyPath - - // Create the http request. - req, err := http.NewRequestWithContext(ctx, http.MethodGet, sURL.String(), nil) - if err != nil { - return xerrors.Errorf("create satellite request: %w", err) - } - res, err := http.DefaultClient.Do(req) - if err != nil { - return xerrors.Errorf("doing satellite request: %w", err) - } - defer func() { _ = res.Body.Close() }() - - if res.StatusCode > 299 { - return fmt.Errorf("unexpected status code %d: %+v", res.StatusCode, res) - } - - var keyRes satelliteKeyResponse - if err := json.NewDecoder(res.Body).Decode(&keyRes); err != nil { - return xerrors.Errorf("decode response body: %w", err) - } - - if keyRes.Key == "" { - return xerrors.New("key field empty in response") - } - if keyRes.Fingerprint == "" { - return xerrors.New("fingerprint field empty in response") - } - - fmt.Printf(`The following satellite will be created: -Name: %s - -Public Key: -%s - -Fingerprint: -%s - -Do you wish to continue? (y/n) -`, name, keyRes.Key, keyRes.Fingerprint) - err = getConfirmation() - if err != nil { - return err - } - - _, err = client.CreateSatellite(ctx, coder.CreateSatelliteReq{ - Name: name, - PublicKey: keyRes.Key, - }) - if err != nil { - return xerrors.Errorf("making create satellite request: %w", err) - } - - clog.LogSuccess(fmt.Sprintf("satellite %s successfully created", name)) - - return nil - }, - } - - return cmd -} - -func getConfirmation() error { - var response string - - _, err := fmt.Scanln(&response) - if err != nil { - return xerrors.Errorf("scan line: %w", err) - } - - response = strings.ToLower(strings.TrimSpace(response)) - if response != "y" && response != "yes" { - return xerrors.New("request canceled") - } - - return nil -} - -func listSatellitesCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "ls", - Short: "list satellites.", - Long: "List all Coder workspace satellites.", - Example: `# list satellites -coder satellites ls`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - client, err := newClient(ctx, true) - if err != nil { - return xerrors.Errorf("making coder client: %w", err) - } - - sats, err := client.Satellites(ctx) - if err != nil { - return xerrors.Errorf("get satellites request: %w", err) - } - - if len(sats) == 0 { - return xerrors.Errorf("no satellites found") - } - - err = tablewriter.WriteTable(cmd.OutOrStdout(), len(sats), func(i int) interface{} { - return sats[i] - }) - if err != nil { - return xerrors.Errorf("write table: %w", err) - } - - return nil - }, - } - return cmd -} - -func deleteSatelliteCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "rm [satellite_name]", - Args: xcobra.ExactArgs(1), - Short: "remove a satellite.", - Long: "Remove an existing Coder satellite by name.", - Example: `# remove an existing satellite by name -coder satellites rm my-satellite`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - name := args[0] - - client, err := newClient(ctx, true) - if err != nil { - return err - } - - sats, err := client.Satellites(ctx) - if err != nil { - return xerrors.Errorf("get satellites request: %w", err) - } - - for _, sat := range sats { - if sat.Name == name { - err = client.DeleteSatelliteByID(ctx, sat.ID) - if err != nil { - return xerrors.Errorf("delete satellites request: %w", err) - } - clog.LogSuccess(fmt.Sprintf("satellite %s successfully deleted", name)) - - return nil - } - } - - return xerrors.Errorf("no satellite found by name '%s'", name) - }, - } - return cmd -} diff --git a/internal/cmd/ssh.go b/internal/cmd/ssh.go deleted file mode 100644 index 983ec3f4..00000000 --- a/internal/cmd/ssh.go +++ /dev/null @@ -1,121 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "os/exec" - "os/user" - "path/filepath" - - "github.com/spf13/cobra" - "golang.org/x/term" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/clog" -) - -var ( - showInteractiveOutput = term.IsTerminal(int(os.Stdout.Fd())) -) - -func sshCmd() *cobra.Command { - cmd := cobra.Command{ - Use: "ssh [workspace_name] []", - Short: "Enter a shell of execute a command over SSH into a Coder workspace", - Args: shValidArgs, - Example: `coder ssh my-dev -coder ssh my-dev pwd`, - Aliases: []string{"sh"}, - DisableFlagParsing: true, - DisableFlagsInUseLine: true, - RunE: shell, - } - return &cmd -} - -func shell(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - - workspace, err := findWorkspace(ctx, client, args[0], coder.Me) - if err != nil { - return err - } - if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { - return clog.Error("workspace not available", - fmt.Sprintf("current status: \"%s\"", workspace.LatestStat.ContainerStatus), - clog.BlankLine, - clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name), - ) - } - wp, err := client.WorkspaceProviderByID(ctx, workspace.ResourcePoolID) - if err != nil { - return err - } - - if !wp.SSHEnabled { - return clog.Error("SSH is disabled on this Workspace") - } - - usr, err := user.Current() - if err != nil { - return xerrors.Errorf("get user home directory: %w", err) - } - privateKeyFilepath := filepath.Join(usr.HomeDir, ".ssh", "coder_enterprise") - - err = writeSSHKey(ctx, client, privateKeyFilepath) - if err != nil { - return err - } - - binPath, err := binPath() - if err != nil { - return xerrors.Errorf("Failed to get executable path: %w", err) - } - - ssh := exec.CommandContext(ctx, - "ssh", "-i"+privateKeyFilepath, - "-o"+fmt.Sprintf("ProxyCommand=%s", proxyCommand(binPath, workspace.Name, false)), - workspace.Name, - ) - if len(args) > 1 { - ssh.Args = append(ssh.Args, args[1:]...) - } - - ssh.Stderr = os.Stderr - ssh.Stdout = os.Stdout - ssh.Stdin = os.Stdin - err = ssh.Run() - var exitErr *exec.ExitError - if xerrors.As(err, &exitErr) { - os.Exit(exitErr.ExitCode()) - return xerrors.New("unreachable") - } - return err -} - -// special handling for the common case of "coder sh" input without a positional argument. -func shValidArgs(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - err := cobra.MinimumNArgs(1)(cmd, args) - if err != nil { - client, err := newClient(ctx, true) - if err != nil { - return clog.Error("missing [workspace_name] argument") - } - _, haystack, err := searchForWorkspace(ctx, client, "", coder.Me) - if err != nil { - return clog.Error("missing [workspace_name] argument", - fmt.Sprintf("specify one of %q", haystack), - clog.BlankLine, - clog.Tipf("run \"coder workspaces ls\" to view your workspaces"), - ) - } - return clog.Error("missing [workspace_name] argument") - } - return nil -} diff --git a/internal/cmd/sync.go b/internal/cmd/sync.go deleted file mode 100644 index eadb3853..00000000 --- a/internal/cmd/sync.go +++ /dev/null @@ -1,121 +0,0 @@ -package cmd - -import ( - "bytes" - "log" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/sync" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/clog" -) - -func syncCmd() *cobra.Command { - var init bool - cmd := &cobra.Command{ - Use: "sync [local directory] [:]", - Short: "Establish a one way directory sync to a Coder workspace", - Args: xcobra.ExactArgs(2), - RunE: makeRunSync(&init), - } - cmd.Flags().BoolVar(&init, "init", false, "do initial transfer and exit") - return cmd -} - -// rsyncVersion returns local rsync protocol version as a string. -func rsyncVersion() string { - cmd := exec.Command("rsync", "--version") - out, err := cmd.CombinedOutput() - if err != nil { - log.Fatal(err) - } - - firstLine, err := bytes.NewBuffer(out).ReadString('\n') - if err != nil { - log.Fatal(err) - } - versionString := strings.Split(firstLine, "protocol version ") - - return versionString[1] -} - -func makeRunSync(init *bool) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - var ( - ctx = cmd.Context() - local = args[0] - remote = args[1] - ) - - client, err := newClient(ctx, true) - if err != nil { - return err - } - - remoteTokens := strings.SplitN(remote, ":", 2) - if len(remoteTokens) != 2 { - return xerrors.New("remote malformatted") - } - var ( - workspaceName = remoteTokens[0] - remoteDir = remoteTokens[1] - ) - - workspace, err := findWorkspace(ctx, client, workspaceName, coder.Me) - if err != nil { - return err - } - - info, err := os.Stat(local) - if err != nil { - return err - } - if info.Mode().IsRegular() { - return sync.SingleFile(ctx, local, remoteDir, workspace, client) - } - if !info.IsDir() { - return xerrors.Errorf("local path must lead to a regular file or directory: %w", err) - } - - absLocal, err := filepath.Abs(local) - if err != nil { - return xerrors.Errorf("make abs path out of %s, %s: %w", local, absLocal, err) - } - - s := sync.Sync{ - Init: *init, - Workspace: *workspace, - RemoteDir: remoteDir, - LocalDir: absLocal, - Client: client, - OutW: cmd.OutOrStdout(), - ErrW: cmd.ErrOrStderr(), - InputReader: cmd.InOrStdin(), - IsInteractiveOutput: showInteractiveOutput, - } - - localVersion := rsyncVersion() - remoteVersion, rsyncErr := s.Version() - - if rsyncErr != nil { - clog.LogInfo("unable to determine remote rsync version: proceeding cautiously") - } else if localVersion != remoteVersion { - return xerrors.Errorf("rsync protocol mismatch: local = %s, remote = %s", localVersion, remoteVersion) - } - - for err == nil || err == sync.ErrRestartSync { - err = s.Run() - } - if err != nil { - return err - } - return nil - } -} diff --git a/internal/cmd/tags.go b/internal/cmd/tags.go deleted file mode 100644 index 24a7affa..00000000 --- a/internal/cmd/tags.go +++ /dev/null @@ -1,173 +0,0 @@ -package cmd - -import ( - "encoding/json" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/clog" - "cdr.dev/coder-cli/pkg/tablewriter" -) - -func tagsCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "tags", - Hidden: true, - Short: "operate on Coder image tags", - } - - cmd.AddCommand( - tagsLsCmd(), - tagsCreateCmd(), - tagsRmCmd(), - ) - return cmd -} - -func tagsCreateCmd() *cobra.Command { - var ( - orgName string - imageName string - defaultTag bool - ) - cmd := &cobra.Command{ - Use: "create [tag]", - Short: "add an image tag", - Long: "allow users to create workspaces with this image tag", - Example: `coder tags create latest --image ubuntu --org default`, - Args: xcobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - img, err := findImg(ctx, client, findImgConf{ - orgName: orgName, - imgName: imageName, - email: coder.Me, - }) - if err != nil { - return xerrors.Errorf("find image: %w", err) - } - - _, err = client.CreateImageTag(ctx, img.ID, coder.CreateImageTagReq{ - Tag: args[0], - Default: defaultTag, - }) - if err != nil { - return xerrors.Errorf("create image tag: %w", err) - } - clog.LogSuccess("created new tag") - - return nil - }, - } - - cmd.Flags().StringVarP(&imageName, "image", "i", "", "image name") - cmd.Flags().StringVarP(&orgName, "org", "o", "", "organization name") - cmd.Flags().BoolVar(&defaultTag, "default", false, "make this tag the default for its image") - _ = cmd.MarkFlagRequired("org") - _ = cmd.MarkFlagRequired("image") - return cmd -} - -func tagsLsCmd() *cobra.Command { - var ( - orgName string - imageName string - outputFmt string - ) - cmd := &cobra.Command{ - Use: "ls", - Example: `coder tags ls --image ubuntu --org default --output json`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - - img, err := findImg(ctx, client, findImgConf{ - email: coder.Me, - orgName: orgName, - imgName: imageName, - }) - if err != nil { - return err - } - - tags, err := client.ImageTags(ctx, img.ID) - if err != nil { - return err - } - - switch outputFmt { - case humanOutput: - err = tablewriter.WriteTable(cmd.OutOrStdout(), len(tags), func(i int) interface{} { return tags[i] }) - if err != nil { - return err - } - case jsonOutput: - err := json.NewEncoder(cmd.OutOrStdout()).Encode(tags) - if err != nil { - return err - } - default: - return clog.Error("unknown --output value") - } - - return nil - }, - } - cmd.Flags().StringVar(&orgName, "org", "", "organization by name") - cmd.Flags().StringVarP(&imageName, "image", "i", "", "image by name") - cmd.Flags().StringVar(&outputFmt, "output", humanOutput, "output format (human|json)") - _ = cmd.MarkFlagRequired("image") - _ = cmd.MarkFlagRequired("org") - return cmd -} - -func tagsRmCmd() *cobra.Command { - var ( - imageName string - orgName string - ) - cmd := &cobra.Command{ - Use: "rm [tag]", - Short: "remove an image tag", - Example: `coder tags rm latest --image ubuntu --org default`, - Args: xcobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - - img, err := findImg(ctx, client, findImgConf{ - email: coder.Me, - imgName: imageName, - orgName: orgName, - }) - if err != nil { - return err - } - - if err = client.DeleteImageTag(ctx, img.ID, args[0]); err != nil { - return err - } - clog.LogSuccess("removed tag") - - return nil - }, - } - cmd.Flags().StringVarP(&orgName, "org", "o", "", "organization by name") - cmd.Flags().StringVarP(&imageName, "image", "i", "", "image by name") - _ = cmd.MarkFlagRequired("image") - _ = cmd.MarkFlagRequired("org") - return cmd -} diff --git a/internal/cmd/tags_test.go b/internal/cmd/tags_test.go deleted file mode 100644 index c04e7098..00000000 --- a/internal/cmd/tags_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package cmd - -import ( - "context" - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/coder-sdk" -) - -func Test_tags(t *testing.T) { - t.Skip("TODO: wait for dedicated test API server / DB so we can create an org") - ctx := context.Background() - - skipIfNoAuth(t) - - res := execute(t, nil, "tags", "ls") - res.error(t) - - ensureImageImported(ctx, t, testCoderClient, "ubuntu", "latest") - - res = execute(t, nil, "tags", "ls", "--image=ubuntu", "--org=default") - res.success(t) - - var tags []coder.ImageTag - res = execute(t, nil, "tags", "ls", "--image=ubuntu", "--org=default", "--output=json") - res.success(t) - res.stdoutUnmarshals(t, &tags) - assert.True(t, "> 0 tags", len(tags) > 0) -} diff --git a/internal/cmd/tokens.go b/internal/cmd/tokens.go deleted file mode 100644 index a014f546..00000000 --- a/internal/cmd/tokens.go +++ /dev/null @@ -1,136 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/tablewriter" -) - -func tokensCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "tokens", - Short: "manage Coder API tokens for the active user", - Long: "Create and manage API Tokens for authenticating the CLI.\n" + - "Statically authenticate using the token value with the " + "`" + "CODER_TOKEN" + "`" + " and " + "`" + "CODER_URL" + "`" + " workspace variables.", - } - cmd.AddCommand( - lsTokensCmd(), - createTokensCmd(), - rmTokenCmd(), - regenTokenCmd(), - ) - return cmd -} - -func lsTokensCmd() *cobra.Command { - var outputFmt string - - cmd := &cobra.Command{ - Use: "ls", - Short: "show the user's active API tokens", - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - - tokens, err := client.APITokens(ctx, coder.Me) - if err != nil { - return err - } - - switch outputFmt { - case humanOutput: - err := tablewriter.WriteTable(cmd.OutOrStdout(), len(tokens), func(i int) interface{} { - return tokens[i] - }) - if err != nil { - return xerrors.Errorf("write table: %w", err) - } - case jsonOutput: - err := json.NewEncoder(cmd.OutOrStdout()).Encode(tokens) - if err != nil { - return xerrors.Errorf("write tokens as JSON: %w", err) - } - default: - return xerrors.Errorf("unknown --output value %q", outputFmt) - } - - return nil - }, - } - - cmd.Flags().StringVarP(&outputFmt, "output", "o", humanOutput, "human | json") - - return cmd -} - -func createTokensCmd() *cobra.Command { - return &cobra.Command{ - Use: "create [token_name]", - Short: "create generates a new API token and prints it to stdout", - Args: xcobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - token, err := client.CreateAPIToken(ctx, coder.Me, coder.CreateAPITokenReq{ - Name: args[0], - }) - if err != nil { - return err - } - fmt.Println(token) - return nil - }, - } -} - -func rmTokenCmd() *cobra.Command { - return &cobra.Command{ - Use: "rm [token_id]", - Short: "remove an API token by its unique ID", - Args: xcobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - if err = client.DeleteAPIToken(ctx, coder.Me, args[0]); err != nil { - return err - } - return nil - }, - } -} - -func regenTokenCmd() *cobra.Command { - return &cobra.Command{ - Use: "regen [token_id]", - Short: "regenerate an API token by its unique ID and print the new token to stdout", - Args: xcobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - token, err := client.RegenerateAPIToken(ctx, coder.Me, args[0]) - if err != nil { - return nil - } - fmt.Println(token) - return nil - }, - } -} diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go deleted file mode 100644 index 203a9786..00000000 --- a/internal/cmd/tunnel.go +++ /dev/null @@ -1,297 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "io" - "net" - "net/url" - "os" - "strconv" - "time" - - "cdr.dev/slog" - "cdr.dev/slog/sloggers/sloghuman" - "github.com/fatih/color" - "github.com/pion/webrtc/v3" - "github.com/spf13/cobra" - "golang.org/x/crypto/ssh" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/clog" - "cdr.dev/coder-cli/wsnet" -) - -func tunnelCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "tunnel [workspace_name] [workspace_port] [localhost_port]", - Args: xcobra.ExactArgs(3), - Short: "proxies a port on the workspace to localhost", - Long: "proxies a port on the workspace to localhost", - Example: `# run a tcp tunnel from the workspace on port 3000 to localhost:3000 - -coder tunnel my-dev 3000 3000 -`, - Hidden: true, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := context.Background() - log := slog.Make(sloghuman.Sink(os.Stderr)) - if os.Getenv("CODER_TUNNEL_DEBUG") != "" { - log = log.Leveled(slog.LevelDebug) - log.Info(ctx, "debug logging enabled") - } - - remotePort, err := strconv.ParseUint(args[1], 10, 16) - if err != nil { - return xerrors.Errorf("parse remote port: %w", err) - } - - var localPort uint64 - if args[2] != "stdio" { - localPort, err = strconv.ParseUint(args[2], 10, 16) - if err != nil { - return xerrors.Errorf("parse local port: %w", err) - } - } - - sdk, err := newClient(ctx, false) - if err != nil { - return xerrors.Errorf("getting coder client: %w", err) - } - baseURL := sdk.BaseURL() - - workspace, err := findWorkspace(ctx, sdk, args[0], coder.Me) - if err != nil { - return xerrors.Errorf("get workspaces: %w", err) - } - - if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { - color.NoColor = false - notAvailableError := clog.Error("workspace not available", - fmt.Sprintf("current status: %q", workspace.LatestStat.ContainerStatus), - clog.BlankLine, - clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name), - ) - // If we're attempting to forward our remote SSH port, - // we want to communicate with the OpenSSH protocol so - // SSH clients can properly display output to our users. - if remotePort == 12213 { - rawKey, err := sdk.SSHKey(ctx) - if err != nil { - return xerrors.Errorf("get ssh key: %w", err) - } - err = discardSSHConnection(&stdioConn{}, rawKey.PrivateKey, notAvailableError.String()) - if err != nil { - return err - } - return nil - } - - return notAvailableError - } - - iceServers, err := sdk.ICEServers(ctx) - if err != nil { - return xerrors.Errorf("get ICE servers: %w", err) - } - log.Debug(ctx, "got ICE servers", slog.F("ice", iceServers)) - - c := &tunnneler{ - log: log, - brokerAddr: &baseURL, - token: sdk.Token(), - workspace: workspace, - iceServers: iceServers, - stdio: args[2] == "stdio", - localPort: uint16(localPort), - remotePort: uint16(remotePort), - } - - err = c.start(ctx) - if err != nil { - return xerrors.Errorf("running tunnel: %w", err) - } - - return nil - }, - } - - return cmd -} - -type tunnneler struct { - log slog.Logger - brokerAddr *url.URL - token string - workspace *coder.Workspace - iceServers []webrtc.ICEServer - remotePort uint16 - localPort uint16 - stdio bool -} - -func (c *tunnneler) start(ctx context.Context) error { - c.log.Debug(ctx, "Connecting to workspace...") - - dialLog := c.log.Named("wsnet") - wd, err := wsnet.DialWebsocket( - ctx, - wsnet.ConnectEndpoint(c.brokerAddr, c.workspace.ID, c.token), - &wsnet.DialOptions{ - Log: &dialLog, - TURNProxyAuthToken: c.token, - TURNRemoteProxyURL: c.brokerAddr, - TURNLocalProxyURL: c.brokerAddr, - ICEServers: c.iceServers, - }, - nil, - ) - if err != nil { - return xerrors.Errorf("creating workspace dialer: %w", err) - } - nc, err := wd.DialContext(ctx, "tcp", fmt.Sprintf("localhost:%d", c.remotePort)) - if err != nil { - return err - } - c.log.Debug(ctx, "Connected to workspace!") - - sdk, err := newClient(ctx, false) - if err != nil { - return xerrors.Errorf("getting coder client: %w", err) - } - - // regularly update the last connection at - go func() { - ticker := time.NewTicker(time.Minute) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - // silently ignore failures so we don't spam the console - _ = sdk.UpdateLastConnectionAt(ctx, c.workspace.ID) - } - } - }() - - // proxy via stdio - if c.stdio { - go func() { - _, _ = io.Copy(nc, os.Stdin) - }() - _, err = io.Copy(os.Stdout, nc) - if err != nil { - return xerrors.Errorf("copy: %w", err) - } - return nil - } - // This was used to test if the port was open, and proxy over stdio - // if the user specified that. - _ = nc.Close() - - // proxy via tcp listener - listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", c.localPort)) - if err != nil { - return xerrors.Errorf("listen: %w", err) - } - - for { - lc, err := listener.Accept() - if err != nil { - return xerrors.Errorf("accept: %w", err) - } - nc, err := wd.DialContext(ctx, "tcp", fmt.Sprintf("localhost:%d", c.remotePort)) - if err != nil { - return err - } - go func() { - defer func() { - _ = lc.Close() - }() - - go func() { - _, _ = io.Copy(lc, nc) - }() - _, _ = io.Copy(nc, lc) - }() - } -} - -// Used to treat stdio like a connection for proxying SSH. -type stdioConn struct{} - -func (s *stdioConn) Read(b []byte) (n int, err error) { - return os.Stdin.Read(b) -} - -func (s *stdioConn) Write(b []byte) (n int, err error) { - return os.Stdout.Write(b) -} - -func (s *stdioConn) Close() error { - return nil -} - -func (s *stdioConn) LocalAddr() net.Addr { - return nil -} - -func (s *stdioConn) RemoteAddr() net.Addr { - return nil -} - -func (s *stdioConn) SetDeadline(t time.Time) error { - return nil -} - -func (s *stdioConn) SetReadDeadline(t time.Time) error { - return nil -} - -func (s *stdioConn) SetWriteDeadline(t time.Time) error { - return nil -} - -// discardSSHConnection accepts a connection then outputs the message provided -// to any channel opened, immediately closing the connection afterwards. -// -// Used to provide status to connecting clients while still aligning with the -// native SSH protocol. -func discardSSHConnection(nc net.Conn, privateKey string, msg string) error { - config := &ssh.ServerConfig{ - NoClientAuth: true, - } - key, err := ssh.ParseRawPrivateKey([]byte(privateKey)) - if err != nil { - return fmt.Errorf("parse private key: %w", err) - } - signer, err := ssh.NewSignerFromKey(key) - if err != nil { - return fmt.Errorf("signer from private key: %w", err) - } - config.AddHostKey(signer) - conn, chans, reqs, err := ssh.NewServerConn(nc, config) - if err != nil { - return fmt.Errorf("create server conn: %w", err) - } - go ssh.DiscardRequests(reqs) - ch, req, err := (<-chans).Accept() - if err != nil { - return fmt.Errorf("accept channel: %w", err) - } - go ssh.DiscardRequests(req) - - _, err = ch.Write([]byte(msg)) - if err != nil { - return fmt.Errorf("write channel: %w", err) - } - err = ch.Close() - if err != nil { - return fmt.Errorf("close channel: %w", err) - } - return conn.Close() -} diff --git a/internal/cmd/update.go b/internal/cmd/update.go deleted file mode 100644 index 124662eb..00000000 --- a/internal/cmd/update.go +++ /dev/null @@ -1,581 +0,0 @@ -package cmd - -import ( - "archive/tar" - "archive/zip" - "bufio" - "bytes" - "compress/gzip" - "context" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/url" - "os" - "os/exec" - "path" - "path/filepath" - "regexp" - "runtime" - "strconv" - "strings" - "time" - - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/internal/config" - "cdr.dev/coder-cli/internal/version" - "cdr.dev/coder-cli/pkg/clog" - - "github.com/Masterminds/semver/v3" - "github.com/manifoldco/promptui" - "github.com/spf13/afero" - "github.com/spf13/cobra" -) - -const ( - goosWindows = "windows" - goosLinux = "linux" - goosDarwin = "darwin" - apiPrivateVersion = "/api/private/version" -) - -// updater updates coder-cli. -type updater struct { - confirmF func(string) (string, error) - execF func(context.Context, string, ...string) ([]byte, error) - executablePath string - fs afero.Fs - httpClient getter - osF func() string - versionF func() string -} - -func updateCmd() *cobra.Command { - var ( - force bool - coderURL string - versionArg string - ) - - cmd := &cobra.Command{ - Use: "update", - Short: "Update coder binary", - Long: "Update coder to the version matching a given coder instance.", - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - httpClient := &http.Client{ - Timeout: 10 * time.Second, - } - - currExe, err := os.Executable() - if err != nil { - return clog.Fatal("init: get current executable", clog.Causef(err.Error())) - } - - updater := &updater{ - confirmF: defaultConfirm, - execF: defaultExec, - executablePath: currExe, - httpClient: httpClient, - fs: afero.NewOsFs(), - osF: func() string { return runtime.GOOS }, - versionF: func() string { return version.Version }, - } - return updater.Run(ctx, force, coderURL, versionArg) - }, - } - - cmd.Flags().BoolVar(&force, "force", false, "do not prompt for confirmation") - cmd.Flags().StringVar(&coderURL, "coder", "", "query this coder instance for the matching version") - cmd.Flags().StringVar(&versionArg, "version", "", "explicitly specify which version to fetch and install") - - return cmd -} - -type getter interface { - Get(url string) (*http.Response, error) -} - -func (u *updater) Run(ctx context.Context, force bool, coderURLArg string, versionArg string) error { - // Check under following directories and warn if coder binary is under them: - // * C:\Windows\ - // * homebrew prefix - // * coder assets root (/var/tmp/coder) - var pathBlockList = []string{ - `C:\Windows\`, - `/var/tmp/coder`, - } - brewPrefixCmd, err := u.execF(ctx, "brew", "--prefix") - if err == nil { // ignore errors if homebrew not installed - pathBlockList = append(pathBlockList, strings.TrimSpace(string(brewPrefixCmd))) - } - - for _, prefix := range pathBlockList { - if HasFilePathPrefix(u.executablePath, prefix) { - return clog.Fatal( - "cowardly refusing to update coder binary", - clog.BlankLine, - clog.Causef("executable path %q is under blocklisted prefix %q", u.executablePath, prefix)) - } - } - - currentBinaryStat, err := u.fs.Stat(u.executablePath) - if err != nil { - return clog.Fatal("preflight: cannot stat current binary", clog.Causef(err.Error())) - } - - if currentBinaryStat.Mode().Perm()&0222 == 0 { - return clog.Fatal("preflight: missing write permission on current binary") - } - - clog.LogInfo(fmt.Sprintf("Current version of coder-cli is %q", version.Version)) - - desiredVersion, err := getDesiredVersion(u.httpClient, coderURLArg, versionArg) - if err != nil { - return clog.Fatal("failed to determine desired version of coder", clog.Causef(err.Error())) - } - - currentVersion, err := semver.NewVersion(u.versionF()) - if err != nil { - clog.LogWarn("failed to determine current version of coder-cli", clog.Causef(err.Error())) - } else if compareVersions(currentVersion, desiredVersion) == 0 { - clog.LogInfo("Up to date!") - return nil - } - - if !force { - prerelease := "" - if desiredVersion.Prerelease() != "" { - prerelease = "-" + desiredVersion.Prerelease() - } - hotfix := "" - if hotfixVersion(desiredVersion) != "" { - hotfix = hotfixVersion(desiredVersion) - } - label := fmt.Sprintf("Do you want to download version %d.%d.%d%s%s instead", - desiredVersion.Major(), - desiredVersion.Minor(), - desiredVersion.Patch(), - prerelease, - hotfix, - ) - if _, err := u.confirmF(label); err != nil { - return clog.Fatal("user cancelled operation", clog.Tipf(`use "--force" to update without confirmation`)) - } - } - - downloadURL, err := queryGithubAssetURL(u.httpClient, desiredVersion, u.osF()) - if err != nil { - return clog.Fatal("failed to query github assets url", clog.Causef(err.Error())) - } - - var downloadBuf bytes.Buffer - memWriter := bufio.NewWriter(&downloadBuf) - - clog.LogInfo("fetching coder-cli from GitHub releases", downloadURL) - resp, err := u.httpClient.Get(downloadURL) - if err != nil { - return clog.Fatal(fmt.Sprintf("failed to fetch URL %s", downloadURL), clog.Causef(err.Error())) - } - - if resp.StatusCode != http.StatusOK { - return clog.Fatal("failed to fetch release", clog.Causef("URL %s returned status code %d", downloadURL, resp.StatusCode)) - } - - if _, err := io.Copy(memWriter, resp.Body); err != nil { - return clog.Fatal(fmt.Sprintf("failed to download %s", downloadURL), clog.Causef(err.Error())) - } - - _ = resp.Body.Close() - - if err := memWriter.Flush(); err != nil { - return clog.Fatal(fmt.Sprintf("failed to save %s", downloadURL), clog.Causef(err.Error())) - } - - // TODO: validate the checksum of the downloaded file. GitHub does not currently provide this information - // and we do not generate them yet. - var updatedBinaryName string - if u.osF() == goosWindows { - updatedBinaryName = "coder.exe" - } else { - updatedBinaryName = "coder" - } - updatedBinary, err := extractFromArchive(updatedBinaryName, downloadBuf.Bytes()) - if err != nil { - return clog.Fatal("failed to extract coder binary from archive", clog.Causef(err.Error())) - } - - // We assume the binary is named coder and write it to coder.new - updatedCoderBinaryPath := u.executablePath + ".new" - updatedBin, err := u.fs.OpenFile(updatedCoderBinaryPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, currentBinaryStat.Mode().Perm()) - if err != nil { - return clog.Fatal("failed to create file for updated coder binary", clog.Causef(err.Error())) - } - - fsWriter := bufio.NewWriter(updatedBin) - if _, err := io.Copy(fsWriter, bytes.NewReader(updatedBinary)); err != nil { - return clog.Fatal("failed to write updated coder binary to disk", clog.Causef(err.Error())) - } - - if err := fsWriter.Flush(); err != nil { - return clog.Fatal("failed to persist updated coder binary to disk", clog.Causef(err.Error())) - } - - _ = updatedBin.Close() - - if err := u.doUpdate(ctx, updatedCoderBinaryPath); err != nil { - return clog.Fatal("failed to update coder binary", clog.Causef(err.Error())) - } - - clog.LogSuccess("Updated coder CLI") - return nil -} - -func (u *updater) doUpdate(ctx context.Context, updatedCoderBinaryPath string) error { - var err error - // TODO(cian): on Windows, we must do two things differently: - // 1) Calling the updated binary fails due to the xterminal.MakeOutputRaw call in main; skipping this check on Windows. - // 2) We must rename the currently running binary before renaming the new binary - if u.osF() == goosWindows { - err = u.fs.Rename(u.executablePath, updatedCoderBinaryPath+".old") - if err != nil { - return xerrors.Errorf("windows: rename current coder binary: %w", err) - } - err = u.fs.Rename(updatedCoderBinaryPath, u.executablePath) - if err != nil { - return xerrors.Errorf("windows: rename updated coder binary: %w", err) - } - return nil - } - - // validate that we can execute the new binary before overwriting - updatedVersionOutput, err := u.execF(ctx, updatedCoderBinaryPath, "--version") - if err != nil { - return xerrors.Errorf("check version of updated coder binary: %w", err) - } - clog.LogInfo(fmt.Sprintf("updated binary reports %q", bytes.TrimSpace(updatedVersionOutput))) - - if err = u.fs.Rename(updatedCoderBinaryPath, u.executablePath); err != nil { - return xerrors.Errorf("update coder binary in-place: %w", err) - } - - return nil -} - -func getDesiredVersion(httpClient getter, coderURLArg string, versionArg string) (*semver.Version, error) { - var coderURL *url.URL - var desiredVersion *semver.Version - var err error - - if coderURLArg != "" && versionArg != "" { - clog.LogWarn(fmt.Sprintf("ignoring the version reported by %q", coderURLArg), clog.Causef("--version flag was specified explicitly")) - } - - if versionArg != "" { - desiredVersion, err = semver.NewVersion(versionArg) - if err != nil { - return &semver.Version{}, xerrors.Errorf("parse desired version arg: %w", err) - } - return desiredVersion, nil - } - - if coderURLArg == "" { - coderURL, err = getCoderConfigURL() - if err != nil { - return &semver.Version{}, xerrors.Errorf("get coder url: %w", err) - } - } else { - coderURL, err = url.Parse(coderURLArg) - if err != nil { - return &semver.Version{}, xerrors.Errorf("parse coder url arg: %w", err) - } - } - - desiredVersion, err = getAPIVersionUnauthed(httpClient, *coderURL) - if err != nil { - return &semver.Version{}, xerrors.Errorf("query coder version: %w", err) - } - - clog.LogInfo(fmt.Sprintf("Coder instance at %q reports version %q", coderURL.String(), desiredVersion.String())) - - return desiredVersion, nil -} - -func defaultConfirm(label string) (string, error) { - p := promptui.Prompt{IsConfirm: true, Label: label} - return p.Run() -} - -func queryGithubAssetURL(httpClient getter, version *semver.Version, ostype string) (string, error) { - var b bytes.Buffer - fmt.Fprintf(&b, "%d", version.Major()) - fmt.Fprint(&b, ".") - fmt.Fprintf(&b, "%d", version.Minor()) - fmt.Fprint(&b, ".") - fmt.Fprintf(&b, "%d", version.Patch()) - if version.Prerelease() != "" { - fmt.Fprint(&b, "-") - fmt.Fprint(&b, version.Prerelease()) - } - fmt.Fprintf(&b, "%s", hotfixVersion(version)) // this will be empty if no hotfix - - urlString := fmt.Sprintf("https://api.github.com/repos/cdr/coder-cli/releases/tags/v%s", b.String()) - clog.LogInfo("query github releases", fmt.Sprintf("url: %q", urlString)) - - type asset struct { - BrowserDownloadURL string `json:"browser_download_url"` - Name string `json:"name"` - } - type release struct { - Assets []asset `json:"assets"` - } - var r release - - resp, err := httpClient.Get(urlString) - if err != nil { - return "", xerrors.Errorf("query github release url %s: %w", urlString, err) - } - defer resp.Body.Close() - - err = json.NewDecoder(resp.Body).Decode(&r) - if err != nil { - return "", xerrors.Errorf("unmarshal github releases api response: %w", err) - } - - var assetURLStr string - for _, a := range r.Assets { - if strings.HasPrefix(a.Name, "coder-cli-"+ostype) { - assetURLStr = a.BrowserDownloadURL - } - } - - if assetURLStr == "" { - return "", xerrors.Errorf("could not find release for ostype %s", ostype) - } - - return assetURLStr, nil -} - -func extractFromArchive(path string, archive []byte) ([]byte, error) { - contentType := http.DetectContentType(archive) - switch contentType { - case "application/zip": - return extractFromZip(path, archive) - case "application/x-gzip": - return extractFromTgz(path, archive) - default: - return nil, xerrors.Errorf("unknown archive type: %s", contentType) - } -} - -func extractFromZip(path string, archive []byte) ([]byte, error) { - zipReader, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive))) - if err != nil { - return nil, xerrors.Errorf("failed to open zip archive") - } - - var zf *zip.File - for _, f := range zipReader.File { - if f.Name == path { - zf = f - break - } - } - if zf == nil { - return nil, xerrors.Errorf("could not find path %q in zip archive", path) - } - - rc, err := zf.Open() - if err != nil { - return nil, xerrors.Errorf("failed to extract path %q from archive", path) - } - defer rc.Close() - - var b bytes.Buffer - bw := bufio.NewWriter(&b) - if _, err := io.Copy(bw, rc); err != nil { - return nil, xerrors.Errorf("failed to copy path %q to from archive", path) - } - return b.Bytes(), nil -} - -func extractFromTgz(path string, archive []byte) ([]byte, error) { - zr, err := gzip.NewReader(bytes.NewReader(archive)) - if err != nil { - return nil, xerrors.Errorf("failed to gunzip archive") - } - - tr := tar.NewReader(zr) - - var b bytes.Buffer - bw := bufio.NewWriter(&b) - for { - hdr, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - return nil, xerrors.Errorf("failed to read tar archive: %w", err) - } - fi := hdr.FileInfo() - if fi.Name() == path && fi.Mode().IsRegular() { - _, err = io.Copy(bw, tr) - if err != nil { - return nil, xerrors.Errorf("failed to read file %q from archive", fi.Name()) - } - break - } - } - - return b.Bytes(), nil -} - -// getCoderConfigURL reads the currently configured coder URL, returning an empty string if not configured. -func getCoderConfigURL() (*url.URL, error) { - urlString, err := config.URL.Read() - if err != nil { - return nil, err - } - configuredURL, err := url.Parse(strings.TrimSpace(urlString)) - if err != nil { - return nil, err - } - return configuredURL, nil -} - -// XXX: coder.Client requires an API key, but we may not be logged into the coder instance for which we -// want to determine the version. We don't need an API key to hit /api/private/version though. -func getAPIVersionUnauthed(client getter, baseURL url.URL) (*semver.Version, error) { - baseURL.Path = path.Join(baseURL.Path, "/api/private/version") - resp, err := client.Get(baseURL.String()) - if err != nil { - return nil, xerrors.Errorf("get %s: %w", baseURL.String(), err) - } - defer resp.Body.Close() - - ver := struct { - Version string `json:"version"` - }{} - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, xerrors.Errorf("read response body: %w", err) - } - - if err := json.Unmarshal(body, &ver); err != nil { - return nil, xerrors.Errorf("parse version response: %w", err) - } - - version, err := semver.NewVersion(ver.Version) - if err != nil { - return nil, xerrors.Errorf("parsing coder version: %w", err) - } - - return version, nil -} - -// HasFilePathPrefix reports whether the filesystem path s -// begins with the elements in prefix. -// Lifted from github.com/golang/go/blob/master/src/cmd/internal/str/path.go. -func HasFilePathPrefix(s, prefix string) bool { - sv := strings.ToUpper(filepath.VolumeName(s)) - pv := strings.ToUpper(filepath.VolumeName(prefix)) - s = s[len(sv):] - prefix = prefix[len(pv):] - switch { - default: - return false - case sv != pv: - return false - case len(s) == len(prefix): - return s == prefix - case prefix == "": - return true - case len(s) > len(prefix): - if prefix[len(prefix)-1] == filepath.Separator { - return strings.HasPrefix(s, prefix) - } - return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix - } -} - -// defaultExec wraps exec.CommandContext. -func defaultExec(ctx context.Context, cmd string, args ...string) ([]byte, error) { - return exec.CommandContext(ctx, cmd, args...).CombinedOutput() -} - -// hotfixExpr matches the build metadata used for identifying CLI hotfixes. -var hotfixExpr = regexp.MustCompile(`(?i)^.*?cli\.(\d+).*?$`) - -// hotfixVersion returns the hotfix build metadata tag if it is present in v -// and an empty string otherwise. -func hotfixVersion(v *semver.Version) string { - match := hotfixExpr.FindStringSubmatch(v.Metadata()) - if len(match) < 2 { - return "" - } - - return fmt.Sprintf("+cli.%s", match[1]) -} - -// compareVersions performs a NON-SEMVER-COMPLIANT comparison of two versions. -// If the two versions differ as per SemVer, then that result is returned. -// Otherwise, the build metadata of the two versions are compared based on -// the `cli.N` hotfix metadata. -// -// Examples: -// compareVersions(semver.MustParse("v1.0.0"), semver.MustParse("v1.0.0")) -// 0 -// compareVersions(semver.MustParse("v1.0.0"), semver.MustParse("v1.0.1")) -// 1 -// compareVersions(semver.MustParse("v1.0.1"), semver.MustParse("v1.0.0")) -// -1 -// compareVersions(semver.MustParse("v1.0.0+cli.0"), semver.MustParse("v1.0.0")) -// 1 -// compareVersions(semver.MustParse("v1.0.0+cli.0"), semver.MustParse("v1.0.0+cli.0")) -// 0 -// compareVersions(semver.MustParse("v1.0.0"), semver.MustParse("v1.0.0+cli.0")) -// -1 -// compareVersions(semver.MustParse("v1.0.0+cli.1"), semver.MustParse("v1.0.0+cli.0")) -// 1 -// compareVersions(semver.MustParse("v1.0.0+cli.0"), semver.MustParse("v1.0.0+cli.1")) -// -1 -// -func compareVersions(a, b *semver.Version) int { - semverComparison := a.Compare(b) - if semverComparison != 0 { - return semverComparison - } - - matchA := hotfixExpr.FindStringSubmatch(a.Metadata()) - matchB := hotfixExpr.FindStringSubmatch(b.Metadata()) - - hotfixA := -1 - hotfixB := -1 - - // extract hotfix versions from the metadata of a and b - if len(matchA) > 1 { - if n, err := strconv.Atoi(matchA[1]); err == nil { - hotfixA = n - } - } - if len(matchB) > 1 { - if n, err := strconv.Atoi(matchB[1]); err == nil { - hotfixB = n - } - } - - // compare hotfix versions - if hotfixA < hotfixB { - return -1 - } - if hotfixA > hotfixB { - return 1 - } - // both versions are the same if their semver and hotfix - // metadata are the same. - return 0 -} diff --git a/internal/cmd/update_test.go b/internal/cmd/update_test.go deleted file mode 100644 index 00855a72..00000000 --- a/internal/cmd/update_test.go +++ /dev/null @@ -1,757 +0,0 @@ -package cmd - -import ( - "archive/tar" - "archive/zip" - "bytes" - "compress/gzip" - "context" - "fmt" - "io" - "io/fs" - "io/ioutil" - "net/http" - "os" - "path/filepath" - "runtime" - "strings" - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" - "github.com/Masterminds/semver/v3" - "github.com/manifoldco/promptui" - "github.com/spf13/afero" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/pkg/clog" -) - -const ( - fakeExePathLinux = "/home/user/bin/coder" - fakeExePathWindows = `C:\Users\user\bin\coder.exe` - fakeCoderURL = "https://my.cdr.dev" - fakeNewVersion = "1.23.4-rc.5+678-gabcdef-12345678" - fakeOldVersion = "1.22.4-rc.5+678-gabcdef-12345678" - fakeHotfixVersion = "1.23.4-rc.5+678-gabcdef-12345678.cli.2" - filenameLinux = "coder-cli-linux-amd64.tar.gz" - filenameWindows = "coder-cli-windows.zip" - fakeGithubReleaseURL = "https://api.github.com/repos/cdr/coder-cli/releases/tags/v1.23.4-rc.5" - fakeGithubHotfixURL = fakeGithubReleaseURL + "+cli.2" -) - -var ( - apiPrivateVersionURL = fakeCoderURL + apiPrivateVersion - fakeError = xerrors.New("fake error for testing") - fakeNewVersionJSON = fmt.Sprintf(`{"version":%q}`, fakeNewVersion) - fakeOldVersionJSON = fmt.Sprintf(`{"version":%q}`, fakeOldVersion) - fakeHotfixVersionJSON = fmt.Sprintf(`{"version":%q}`, fakeHotfixVersion) - fakeNewVersionTgz = mustValidTgz("coder", []byte(fakeNewVersion), 0751) - fakeHotfixVersionTgz = mustValidTgz("coder", []byte(fakeHotfixVersion), 0751) - fakeNewVersionZip = mustValidZip("coder.exe", []byte(fakeNewVersion)) - fakeHotfixVersionZip = mustValidZip("coder.exe", []byte(fakeHotfixVersion)) - fakeAssetURLLinux = "https://github.com/cdr/coder-cli/releases/download/v1.23.4-rc.5/" + filenameLinux - fakeAssetURLWindows = "https://github.com/cdr/coder-cli/releases/download/v1.23.4-rc.5/" + filenameWindows - fakeHotfixURLLinux = "https://github.com/cdr/coder-cli/releases/download/v1.23.4-rc.5+cli.2/" + filenameLinux - fakeHotfixURLWindows = "https://github.com/cdr/coder-cli/releases/download/v1.23.4-rc.5+cli.2/" + filenameWindows -) - -func Test_updater_run(t *testing.T) { - t.Parallel() - - // params holds parameters for each test case - type params struct { - ConfirmF func(string) (string, error) - Ctx context.Context - Execer *fakeExecer - ExecutablePath string - Fakefs afero.Fs - HTTPClient *fakeGetter - OsF func() string - VersionF func() string - } - - // fromParams creates a new updater from params - fromParams := func(p *params) *updater { - return &updater{ - confirmF: p.ConfirmF, - execF: p.Execer.ExecF, - executablePath: p.ExecutablePath, - fs: p.Fakefs, - httpClient: p.HTTPClient, - osF: p.OsF, - versionF: p.VersionF, - } - } - - run := func(t *testing.T, name string, fn func(t *testing.T, p *params)) { - t.Run(name, func(t *testing.T) { - t.Logf("running %s", name) - ctx := context.Background() - fakefs := afero.NewMemMapFs() - execer := newFakeExecer(t) - execer.M["brew --prefix"] = fakeExecerResult{[]byte{}, os.ErrNotExist} - params := ¶ms{ - // This must be overridden inside run() - ConfirmF: func(string) (string, error) { - t.Errorf("unhandled ConfirmF") - t.FailNow() - return "", nil - }, - Execer: execer, - Ctx: ctx, - ExecutablePath: fakeExePathLinux, - Fakefs: fakefs, - HTTPClient: newFakeGetter(t), - // Default to GOOS=linux - OsF: func() string { return goosLinux }, - // This must be overridden inside run() - VersionF: func() string { - t.Errorf("unhandled VersionF") - t.FailNow() - return "" - }, - } - - fn(t, params) - }) - } - - run(t, "update coder - noop", func(t *testing.T, p *params) { - fakeNewVersion := "v" + fakeNewVersion - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeNewVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.VersionF = func() string { return fakeNewVersion } - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assert.Success(t, "update coder - noop", err) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) - }) - - run(t, "update coder - should be noop but versions have leading v", func(t *testing.T, p *params) { - fakeNewVersion := "v" + fakeNewVersion - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeNewVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.VersionF = func() string { return fakeNewVersion } - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assert.Success(t, "update coder - noop", err) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) - }) - - run(t, "update coder - explicit version specified", func(t *testing.T, p *params) { - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeOldVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) - p.VersionF = func() string { return fakeOldVersion } - p.ConfirmF = fakeConfirmYes - p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, fakeNewVersion) - assert.Success(t, "update coder - explicit version specified", err) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) - }) - - run(t, "update coder - explicit version - leading v", func(t *testing.T, p *params) { - fakeNewVersion := "v" + fakeNewVersion - fakeOldVersion := "v" + fakeOldVersion - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeOldVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) - p.VersionF = func() string { return fakeOldVersion } - p.ConfirmF = fakeConfirmYes - p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, fakeNewVersion) - assert.Success(t, "update coder - explicit version specified", err) - assertFileContent(t, p.Fakefs, fakeExePathLinux, strings.TrimPrefix(fakeNewVersion, "v")) // TODO: stop hard-coding this - }) - - run(t, "update coder - old to new", func(t *testing.T, p *params) { - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) - p.VersionF = func() string { return fakeOldVersion } - p.ConfirmF = fakeConfirmYes - p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assert.Success(t, "update coder - old to new", err) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) - }) - - run(t, "update coder - old to new - leading v", func(t *testing.T, p *params) { - fakeNewVersion := "v" + fakeNewVersion - fakeOldVersion := "v" + fakeOldVersion - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) - p.VersionF = func() string { return fakeOldVersion } - p.ConfirmF = fakeConfirmYes - p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assert.Success(t, "update coder - old to new", err) - assertFileContent(t, p.Fakefs, fakeExePathLinux, strings.TrimPrefix(fakeNewVersion, "v")) // TODO: stop hard-coding this - }) - - run(t, "update coder - new to hotfix", func(t *testing.T, p *params) { - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeNewVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeHotfixVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubHotfixURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeHotfixURLLinux), 200, variadicS(), nil) - p.HTTPClient.M[fakeHotfixURLLinux] = newFakeGetterResponse(fakeHotfixVersionTgz, 200, variadicS(), nil) - p.VersionF = func() string { return fakeNewVersion } - p.ConfirmF = fakeConfirmYes - p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assert.Success(t, "update coder - new to hotfix", err) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeHotfixVersion) - }) - - run(t, "update coder - new to hotfix - windows", func(t *testing.T, p *params) { - p.OsF = func() string { return goosWindows } - p.ExecutablePath = fakeExePathWindows - fakeFile(t, p.Fakefs, fakeExePathWindows, 0755, fakeNewVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeHotfixVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubHotfixURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameWindows, fakeHotfixURLWindows), 200, variadicS(), nil) - p.HTTPClient.M[fakeHotfixURLWindows] = newFakeGetterResponse(fakeHotfixVersionZip, 200, variadicS(), nil) - p.VersionF = func() string { return fakeNewVersion } - p.ConfirmF = fakeConfirmYes - p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathWindows, fakeNewVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assert.Success(t, "update coder - new to hotfix", err) - assertFileContent(t, p.Fakefs, fakeExePathWindows, fakeHotfixVersion) - }) - - run(t, "update coder - old to new - binary renamed", func(t *testing.T, p *params) { - p.ExecutablePath = "/home/user/bin/coder-cli" - fakeFile(t, p.Fakefs, p.ExecutablePath, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) - p.VersionF = func() string { return fakeOldVersion } - p.ConfirmF = fakeConfirmYes - p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} - u := fromParams(p) - assertFileContent(t, p.Fakefs, p.ExecutablePath, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assert.Success(t, "update coder - old to new - binary renamed", err) - assertFileContent(t, p.Fakefs, p.ExecutablePath, fakeNewVersion) - }) - - run(t, "update coder - old to new - windows", func(t *testing.T, p *params) { - p.OsF = func() string { return goosWindows } - p.ExecutablePath = fakeExePathWindows - fakeFile(t, p.Fakefs, fakeExePathWindows, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameWindows, fakeAssetURLWindows), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLWindows] = newFakeGetterResponse(fakeNewVersionZip, 200, variadicS(), nil) - p.VersionF = func() string { return fakeOldVersion } - p.ConfirmF = fakeConfirmYes - p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathWindows, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assert.Success(t, "update coder - old to new - windows", err) - assertFileContent(t, p.Fakefs, fakeExePathWindows, fakeNewVersion) - }) - - run(t, "update coder - old to new forced", func(t *testing.T, p *params) { - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) - p.VersionF = func() string { return fakeOldVersion } - p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{[]byte(fakeNewVersion), nil} - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, true, fakeCoderURL, "") - assert.Success(t, "update coder - old to new forced", err) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeNewVersion) - }) - - run(t, "update coder - user cancelled", func(t *testing.T, p *params) { - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.VersionF = func() string { return fakeOldVersion } - p.ConfirmF = fakeConfirmNo - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - user cancelled", err, "user cancelled operation", "") - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - }) - - run(t, "update coder - cannot stat", func(t *testing.T, p *params) { - u := fromParams(p) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - cannot stat", err, "cannot stat current binary", os.ErrNotExist.Error()) - }) - - run(t, "update coder - no permission", func(t *testing.T, p *params) { - fakeFile(t, p.Fakefs, fakeExePathLinux, 0400, fakeOldVersion) - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - no permission", err, "missing write permission", "") - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - }) - - run(t, "update coder - invalid version arg", func(t *testing.T, p *params) { - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) - p.VersionF = func() string { return fakeOldVersion } - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "Invalid Semantic Version") - assertCLIError(t, "update coder - invalid version arg", err, "failed to determine desired version of coder", "Invalid Semantic Version") - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - }) - - run(t, "update coder - invalid url", func(t *testing.T, p *params) { - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) - p.VersionF = func() string { return fakeOldVersion } - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, false, "h$$p://invalid.url", "") - assertCLIError(t, "update coder - invalid url", err, "failed to determine desired version of coder", "first path segment in URL cannot contain colon") - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - }) - - run(t, "update coder - fetch api version failure", func(t *testing.T, p *params) { - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte{}, 401, variadicS(), fakeError) - p.VersionF = func() string { return fakeOldVersion } - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - fetch api version failure", err, "failed to determine desired version of coder", fakeError.Error()) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - }) - - run(t, "update coder - failed to query github releases", func(t *testing.T, p *params) { - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse([]byte{}, 0, variadicS(), fakeError) - p.VersionF = func() string { return fakeOldVersion } - p.ConfirmF = fakeConfirmYes - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - failed to query github releases", err, "failed to query github assets", fakeError.Error()) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - }) - - run(t, "update coder - failed to fetch URL", func(t *testing.T, p *params) { - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse([]byte{}, 0, variadicS(), fakeError) - p.VersionF = func() string { return fakeOldVersion } - p.ConfirmF = fakeConfirmYes - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - failed to fetch URL", err, "failed to fetch URL", fakeError.Error()) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - }) - - run(t, "update coder - release URL 404", func(t *testing.T, p *params) { - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse([]byte{}, 404, variadicS(), nil) - p.VersionF = func() string { return fakeOldVersion } - p.ConfirmF = fakeConfirmYes - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - release URL 404", err, "failed to fetch release", "status code 404") - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - }) - - run(t, "update coder - invalid tgz archive", func(t *testing.T, p *params) { - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse([]byte{}, 200, variadicS(), nil) - p.VersionF = func() string { return fakeOldVersion } - p.ConfirmF = fakeConfirmYes - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - invalid tgz archive", err, "failed to extract coder binary from archive", "unknown archive type") - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - }) - - run(t, "update coder - invalid zip archive", func(t *testing.T, p *params) { - p.OsF = func() string { return goosWindows } - p.ExecutablePath = fakeExePathWindows - fakeFile(t, p.Fakefs, fakeExePathWindows, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameWindows, fakeAssetURLWindows), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLWindows] = newFakeGetterResponse([]byte{}, 200, variadicS(), nil) - p.VersionF = func() string { return fakeOldVersion } - p.ConfirmF = fakeConfirmYes - u := fromParams(p) - assertFileContent(t, p.Fakefs, p.ExecutablePath, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - invalid zip archive", err, "failed to extract coder binary from archive", "unknown archive type") - assertFileContent(t, p.Fakefs, p.ExecutablePath, fakeOldVersion) - }) - - run(t, "update coder - read-only fs", func(t *testing.T, p *params) { - rwfs := p.Fakefs - p.Fakefs = afero.NewReadOnlyFs(rwfs) - fakeFile(t, rwfs, fakeExePathLinux, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) - p.VersionF = func() string { return fakeOldVersion } - p.ConfirmF = fakeConfirmYes - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - read-only fs", err, "failed to create file", "") - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - }) - - run(t, "update coder - cannot exec new binary", func(t *testing.T, p *params) { - fakeFile(t, p.Fakefs, fakeExePathLinux, 0755, fakeOldVersion) - p.HTTPClient.M[apiPrivateVersionURL] = newFakeGetterResponse([]byte(fakeNewVersionJSON), 200, variadicS(), nil) - p.HTTPClient.M[fakeGithubReleaseURL] = newFakeGetterResponse(fakeGithubReleaseJSON(filenameLinux, fakeAssetURLLinux), 200, variadicS(), nil) - p.HTTPClient.M[fakeAssetURLLinux] = newFakeGetterResponse(fakeNewVersionTgz, 200, variadicS(), nil) - p.VersionF = func() string { return fakeOldVersion } - p.ConfirmF = fakeConfirmYes - p.Execer.M[p.ExecutablePath+".new --version"] = fakeExecerResult{nil, fakeError} - u := fromParams(p) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - cannot exec new binary", err, "failed to update coder binary", fakeError.Error()) - assertFileContent(t, p.Fakefs, fakeExePathLinux, fakeOldVersion) - }) - - if runtime.GOOS == goosWindows { - run(t, "update coder - path blocklist - windows", func(t *testing.T, p *params) { - p.ExecutablePath = `C:\Windows\system32\coder.exe` - u := fromParams(p) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - path blocklist - windows", err, "cowardly refusing to update coder binary", "blocklisted prefix") - }) - } else { - run(t, "update coder - path blocklist - coder assets dir", func(t *testing.T, p *params) { - p.ExecutablePath = `/var/tmp/coder/coder` - u := fromParams(p) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - path blocklist - windows", err, "cowardly refusing to update coder binary", "blocklisted prefix") - }) - run(t, "update coder - path blocklist - old homebrew prefix", func(t *testing.T, p *params) { - p.Execer.M["brew --prefix"] = fakeExecerResult{[]byte("/usr/local"), nil} - p.ExecutablePath = `/usr/local/bin/coder` - u := fromParams(p) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - path blocklist - old homebrew prefix", err, "cowardly refusing to update coder binary", "blocklisted prefix") - }) - run(t, "update coder - path blocklist - new homebrew prefix", func(t *testing.T, p *params) { - p.Execer.M["brew --prefix"] = fakeExecerResult{[]byte("/opt/homebrew"), nil} - p.ExecutablePath = `/opt/homebrew/bin/coder` - u := fromParams(p) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - path blocklist - new homebrew prefix", err, "cowardly refusing to update coder binary", "blocklisted prefix") - }) - run(t, "update coder - path blocklist - linuxbrew", func(t *testing.T, p *params) { - p.Execer.M["brew --prefix"] = fakeExecerResult{[]byte("/home/user/.linuxbrew"), nil} - p.ExecutablePath = `/home/user/.linuxbrew/bin/coder` - u := fromParams(p) - err := u.Run(p.Ctx, false, fakeCoderURL, "") - assertCLIError(t, "update coder - path blocklist - linuxbrew", err, "cowardly refusing to update coder binary", "blocklisted prefix") - }) - } -} - -func Test_getDesiredVersion(t *testing.T) { - t.Parallel() - - t.Run("invalid version specified by user", func(t *testing.T) { - t.Parallel() - - expected := &semver.Version{} - actual, err := getDesiredVersion(nil, "", "not a valid version") - assert.ErrorContains(t, "error should be nil", err, "Invalid Semantic Version") - assert.Equal(t, "expected should equal actual", expected, actual) - }) - - t.Run("underspecified version from user", func(t *testing.T) { - t.Parallel() - - expected, err := semver.StrictNewVersion("1.23.0") - assert.Success(t, "error should be nil", err) - actual, err := getDesiredVersion(nil, "", "1.23") - assert.Success(t, "error should be nil", err) - assert.True(t, "should handle versions without trailing zero", expected.Equal(actual)) - }) -} - -func Test_compareVersions(t *testing.T) { - t.Parallel() - - testCases := []struct { - Name string - V1 string - V2 string - Expected int - }{ - {"old vs old", fakeOldVersion, fakeOldVersion, 0}, - {"old vs new", fakeOldVersion, fakeNewVersion, -1}, - {"old vs hotfix", fakeOldVersion, fakeHotfixVersion, -1}, - {"new vs old", fakeNewVersion, fakeOldVersion, 1}, - {"new vs new", fakeNewVersion, fakeNewVersion, 0}, - {"new vs hotfix", fakeNewVersion, fakeHotfixVersion, -1}, - {"hotfix vs old", fakeHotfixVersion, fakeOldVersion, 1}, - {"hotfix vs new", fakeHotfixVersion, fakeNewVersion, 1}, - {"hotfix vs hotfix", fakeHotfixVersion, fakeHotfixVersion, 0}, - } - for _, testCase := range testCases { - testCase := testCase - v1 := semver.MustParse(testCase.V1) - v2 := semver.MustParse(testCase.V2) - actual := compareVersions(v1, v2) - assert.Equal(t, testCase.Name+": expected comparison differs", testCase.Expected, actual) - } -} - -// fakeGetter mocks HTTP requests. -type fakeGetter struct { - M map[string]*fakeGetterResponse - T *testing.T -} - -func newFakeGetter(t *testing.T) *fakeGetter { - return &fakeGetter{ - M: make(map[string]*fakeGetterResponse), - T: t, - } -} - -// Get returns the configured response for url. If no response configured, test fails immediately. -func (f *fakeGetter) Get(url string) (*http.Response, error) { - f.T.Helper() - val, ok := f.M[url] - if !ok { - f.T.Errorf("unhandled url: %s", url) - f.T.FailNow() - return nil, nil // this will never happen - } - return val.Resp, val.Err -} - -type fakeGetterResponse struct { - Resp *http.Response - Err error -} - -// newFakeGetterResponse is a convenience function for mocking HTTP requests. -func newFakeGetterResponse(body []byte, code int, headers []string, err error) *fakeGetterResponse { - resp := &http.Response{} - resp.Body = ioutil.NopCloser(bytes.NewReader(body)) - resp.StatusCode = code - resp.Header = http.Header{} - - for _, e := range headers { - parts := strings.Split(e, ":") - k := strings.ToLower(strings.TrimSpace(parts[0])) - v := strings.ToLower(strings.TrimSpace(strings.Join(parts[1:], ":"))) - resp.Header.Set(k, v) - } - - return &fakeGetterResponse{ - Resp: resp, - Err: err, - } -} - -func variadicS(s ...string) []string { - return s -} - -func fakeConfirmYes(_ string) (string, error) { - return "y", nil -} - -func fakeConfirmNo(_ string) (string, error) { - return "", promptui.ErrAbort -} - -func fakeFile(t *testing.T, fs afero.Fs, name string, perm fs.FileMode, content string) { - t.Helper() - err := fs.MkdirAll(filepath.Dir(name), 0750) - if err != nil { - panic(err) - } - f, err := fs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm) - if err != nil { - panic(err) - } - defer f.Close() - - _, err = f.Write([]byte(content)) - if err != nil { - panic(err) - } -} - -func assertFileContent(t *testing.T, fs afero.Fs, name string, content string) { - t.Helper() - f, err := fs.OpenFile(name, os.O_RDONLY, 0) - assert.Success(t, "open file "+name, err) - defer f.Close() - - b, err := ioutil.ReadAll(f) - assert.Success(t, "read file "+name, err) - - assert.Equal(t, "assert content equal", content, string(b)) -} - -func assertCLIError(t *testing.T, name string, err error, expectedHeader, expectedLines string) { - t.Helper() - cliError, ok := err.(clog.CLIError) - if !ok { - t.Errorf("%s: assert cli error: %+v is not a cli error", name, err) - } - - if !strings.Contains(err.Error(), expectedHeader) { - t.Errorf("%s: assert cli error: expected header %q to contain %q", name, err.Error(), expectedHeader) - } - - if expectedLines == "" { - return - } - - fullLines := strings.Join(cliError.Lines, "\n") - if !strings.Contains(fullLines, expectedLines) { - t.Errorf("%s: assert cli error: expected %q to contain %q", name, fullLines, expectedLines) - } -} - -// mustValidTgz creates a valid tgz file and panics if any error is encountered. -// only for use in unit tests. -func mustValidTgz(filename string, data []byte, perms os.FileMode) []byte { - must := func(err error, msg string) { - if err != nil { - panic(xerrors.Errorf("%s: %w", msg, err)) - } - } - fs := afero.NewMemMapFs() - // populate memfs with file - f, err := fs.Create(filename) - must(err, "create file") - _, err = f.Write(data) - must(err, "write data") - err = f.Close() - must(err, "close file") - err = fs.Chmod(filename, perms) - must(err, "set perms") - - // create archive from fs - - f, err = fs.Open(filename) - must(err, "open file") - fsinfo, err := f.Stat() - must(err, "stat file") - header, err := tar.FileInfoHeader(fsinfo, fsinfo.Name()) - must(err, "create tar header") - header.Name = filename - - var buf bytes.Buffer - gw := gzip.NewWriter(&buf) - tw := tar.NewWriter(gw) - - err = tw.WriteHeader(header) - must(err, "write header") - _, err = io.Copy(tw, f) - must(err, "write file") - err = f.Close() - must(err, "close file") - err = tw.Close() - must(err, "close tar writer") - err = gw.Close() - must(err, "close gzip writer") - - return buf.Bytes() -} - -// mustValidZip creates a valid zip file and panics if any error is encountered. -// only for use in unit tests. -func mustValidZip(filename string, data []byte) []byte { - must := func(err error, msg string) { - if err != nil { - panic(xerrors.Errorf("%s: %w", msg, err)) - } - } - var buf bytes.Buffer - zw := zip.NewWriter(&buf) - w, err := zw.Create(filename) - must(err, "create zip archive") - _, err = io.Copy(w, bytes.NewReader(data)) - must(err, "write file") - err = zw.Close() - must(err, "close gzip writer") - - return buf.Bytes() -} - -var _ = mustValidTgz("testing", []byte("testing"), 0777) -var _ = mustValidZip("testing", []byte("testing")) - -type fakeExecer struct { - M map[string]fakeExecerResult - T *testing.T -} - -func (f *fakeExecer) ExecF(_ context.Context, cmd string, args ...string) ([]byte, error) { - cmdAndArgs := strings.Join(append([]string{cmd}, args...), " ") - val, ok := f.M[cmdAndArgs] - if !ok { - f.T.Errorf("unhandled cmd %q", cmd) - f.T.FailNow() - return nil, nil // will never happen - } - return val.Output, val.Err -} - -func newFakeExecer(t *testing.T) *fakeExecer { - return &fakeExecer{ - M: make(map[string]fakeExecerResult), - T: t, - } -} - -type fakeExecerResult struct { - Output []byte - Err error -} - -func fakeGithubReleaseJSON(filename, assetURL string) []byte { - jsonStr := fmt.Sprintf(` - {"assets": - [ - { - "name": %q, - "browser_download_url": %q - } - ] - }`, filename, assetURL) - return []byte(jsonStr) -} diff --git a/internal/cmd/urls.go b/internal/cmd/urls.go deleted file mode 100644 index b20d1794..00000000 --- a/internal/cmd/urls.go +++ /dev/null @@ -1,270 +0,0 @@ -package cmd - -import ( - "context" - "encoding/json" - "fmt" - "regexp" - "strconv" - "strings" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/clog" - "cdr.dev/coder-cli/pkg/tablewriter" -) - -func urlCmd() *cobra.Command { - var outputFmt string - cmd := &cobra.Command{ - Use: "urls", - Short: "Interact with workspace DevURLs", - } - lsCmd := &cobra.Command{ - Use: "ls [workspace_name]", - Short: "List all DevURLs for a workspace", - Args: xcobra.ExactArgs(1), - RunE: listDevURLsCmd(&outputFmt), - } - lsCmd.Flags().StringVarP(&outputFmt, "output", "o", humanOutput, "human|json") - - rmCmd := &cobra.Command{ - Use: "rm [workspace_name] [port]", - Args: cobra.ExactArgs(2), - Short: "Remove a dev url", - RunE: removeDevURL, - } - - cmd.AddCommand( - lsCmd, - rmCmd, - createDevURLCmd(), - ) - - return cmd -} - -var urlAccessLevel = map[string]string{ - // Remote API endpoint requires these in uppercase. - "PRIVATE": "Only you can access", - "ORG": "All members of your organization can access", - "AUTHED": "Authenticated users can access", - "PUBLIC": "Anyone on the internet can access this link", -} - -func validatePort(port string) (int, error) { - p, err := strconv.ParseUint(port, 10, 16) - if err != nil { - clog.Log(clog.Error("invalid port")) - return 0, err - } - if p < 1 { - // Port 0 means 'any free port', which we don't support. - return 0, xerrors.New("Port must be > 0") - } - return int(p), nil -} - -func accessLevelIsValid(level string) bool { - _, ok := urlAccessLevel[level] - if !ok { - clog.Log(clog.Error("invalid access level")) - } - return ok -} - -// Run gets the list of active devURLs from the cemanager for the -// specified workspace and outputs info to stdout. -func listDevURLsCmd(outputFmt *string) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - workspaceName := args[0] - - devURLs, err := urlList(ctx, client, workspaceName) - if err != nil { - return err - } - - switch *outputFmt { - case humanOutput: - if len(devURLs) < 1 { - clog.LogInfo(fmt.Sprintf("no devURLs found for workspace %q", workspaceName)) - return nil - } - err := tablewriter.WriteTable(cmd.OutOrStdout(), len(devURLs), func(i int) interface{} { - return devURLs[i] - }) - if err != nil { - return xerrors.Errorf("write table: %w", err) - } - case jsonOutput: - if err := json.NewEncoder(cmd.OutOrStdout()).Encode(devURLs); err != nil { - return xerrors.Errorf("encode DevURLs as json: %w", err) - } - default: - return xerrors.Errorf("unknown --output value %q", *outputFmt) - } - return nil - } -} - -func createDevURLCmd() *cobra.Command { - var ( - access string - urlname string - scheme string - ) - cmd := &cobra.Command{ - Use: "create [workspace_name] [port]", - Short: "Create a new dev URL for a workspace", - Aliases: []string{"edit"}, - Args: xcobra.ExactArgs(2), - Example: `coder urls create my-workspace 8080 --name my-dev-url`, - RunE: func(cmd *cobra.Command, args []string) error { - var ( - workspaceName = args[0] - port = args[1] - ctx = cmd.Context() - ) - - portNum, err := validatePort(port) - if err != nil { - return err - } - - access = strings.ToUpper(access) - if !accessLevelIsValid(access) { - return xerrors.Errorf("invalid access level %q", access) - } - - if urlname != "" && !devURLValidNameRx.MatchString(urlname) { - return xerrors.Errorf(devURLInvalidNameMsg, urlname) - } - client, err := newClient(ctx, true) - if err != nil { - return err - } - - workspace, err := findWorkspace(ctx, client, workspaceName, coder.Me) - if err != nil { - return err - } - - urls, err := urlList(ctx, client, workspaceName) - if err != nil { - return err - } - - urlID, found := devURLID(portNum, urls) - if found { - err := client.PutDevURL(ctx, workspace.ID, urlID, coder.PutDevURLReq{ - Port: portNum, - Name: urlname, - Access: access, - WorkspaceID: workspace.ID, - Scheme: scheme, - }) - if err != nil { - return xerrors.Errorf("update DevURL: %w", err) - } - clog.LogSuccess(fmt.Sprintf("patched devurl for port %s", port)) - } else { - err := client.CreateDevURL(ctx, workspace.ID, coder.CreateDevURLReq{ - Port: portNum, - Name: urlname, - Access: access, - WorkspaceID: workspace.ID, - Scheme: scheme, - }) - if err != nil { - return xerrors.Errorf("insert DevURL: %w", err) - } - clog.LogSuccess(fmt.Sprintf("created devurl for port %s", port)) - } - return nil - }, - } - - cmd.Flags().StringVar(&access, "access", "private", "Set DevURL access to [private | org | authed | public]") - cmd.Flags().StringVar(&urlname, "name", "", "DevURL name") - cmd.Flags().StringVar(&scheme, "scheme", "http", "Server scheme (http|https)") - return cmd -} - -// devURLNameValidRx is the regex used to validate devurl names specified -// via the --name subcommand. Named devurls must begin with a letter -// followed by zero or more letters, numbers, hyphens, or underscores, -// end with a letter or a number, and be maximum 64 characters in length. -// The maximum length of the name component is 43 characters. -var devURLValidNameRx = regexp.MustCompile("^[a-zA-Z]([a-zA-Z0-9_-]{0,41}[a-zA-Z0-9])?$") -var devURLInvalidNameMsg = "invalid devurl name %q: names must begin with a letter, " + - "followed by zero or more letters, digits, hyphens, or underscores, and end with a " + - "letter or digit, and be a maximum of 43 characters in length." - -// devURLID returns the ID of a devURL, given the workspace name and port -// from a list of DevURL records. -// ("", false) is returned if no match is found. -func devURLID(port int, urls []coder.DevURL) (string, bool) { - for _, url := range urls { - if url.Port == port { - return url.ID, true - } - } - return "", false -} - -// Run deletes a devURL, specified by workspace ID and port, from the cemanager. -func removeDevURL(cmd *cobra.Command, args []string) error { - var ( - workspaceName = args[0] - port = args[1] - ctx = cmd.Context() - ) - - portNum, err := validatePort(port) - if err != nil { - return xerrors.Errorf("validate port: %w", err) - } - - client, err := newClient(ctx, true) - if err != nil { - return err - } - workspace, err := findWorkspace(ctx, client, workspaceName, coder.Me) - if err != nil { - return err - } - - urls, err := urlList(ctx, client, workspaceName) - if err != nil { - return err - } - - urlID, found := devURLID(portNum, urls) - if found { - clog.LogInfo(fmt.Sprintf("deleting devurl for port %v", port)) - } else { - return xerrors.Errorf("No devurl found for port %v", port) - } - - if err := client.DeleteDevURL(ctx, workspace.ID, urlID); err != nil { - return xerrors.Errorf("delete DevURL: %w", err) - } - return nil -} - -// urlList returns the list of active devURLs from the cemanager. -func urlList(ctx context.Context, client coder.Client, workspaceName string) ([]coder.DevURL, error) { - workspace, err := findWorkspace(ctx, client, workspaceName, coder.Me) - if err != nil { - return nil, err - } - return client.DevURLs(ctx, workspace.ID) -} diff --git a/internal/cmd/users.go b/internal/cmd/users.go deleted file mode 100644 index c9a00343..00000000 --- a/internal/cmd/users.go +++ /dev/null @@ -1,61 +0,0 @@ -package cmd - -import ( - "encoding/json" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/pkg/tablewriter" -) - -func usersCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "users", - Short: "Interact with Coder user accounts", - } - - var outputFmt string - lsCmd := &cobra.Command{ - Use: "ls", - Short: "list all user accounts", - Example: `coder users ls -o json -coder users ls -o json | jq .[] | jq -r .email`, - RunE: listUsers(&outputFmt), - } - lsCmd.Flags().StringVarP(&outputFmt, "output", "o", humanOutput, "human | json") - - cmd.AddCommand(lsCmd) - return cmd -} - -func listUsers(outputFmt *string) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - - users, err := client.Users(ctx) - if err != nil { - return xerrors.Errorf("get users: %w", err) - } - - switch *outputFmt { - case humanOutput: - // For each element, return the user. - each := func(i int) interface{} { return users[i] } - if err := tablewriter.WriteTable(cmd.OutOrStdout(), len(users), each); err != nil { - return xerrors.Errorf("write table: %w", err) - } - case "json": - if err := json.NewEncoder(cmd.OutOrStdout()).Encode(users); err != nil { - return xerrors.Errorf("encode users as json: %w", err) - } - default: - return xerrors.New("unknown value for --output") - } - return nil - } -} diff --git a/internal/cmd/users_test.go b/internal/cmd/users_test.go deleted file mode 100644 index dad57ed9..00000000 --- a/internal/cmd/users_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package cmd - -import ( - "testing" - - "cdr.dev/slog" - "cdr.dev/slog/sloggers/slogtest" - - "cdr.dev/coder-cli/coder-sdk" -) - -func Test_users(t *testing.T) { - skipIfNoAuth(t) - - var users []coder.User - res := execute(t, nil, "users", "ls", "--output=json") - res.success(t) - res.stdoutUnmarshals(t, &users) - assertAdmin(t, users) - - res = execute(t, nil, "users", "ls", "--output=human") - res.success(t) -} - -func assertAdmin(t *testing.T, users []coder.User) { - for _, u := range users { - if u.Username == "kyle" { - return - } - } - slogtest.Fatal(t, "did not find kyle user", slog.F("users", users)) -} diff --git a/internal/cmd/workspaces.go b/internal/cmd/workspaces.go deleted file mode 100644 index 8ac30565..00000000 --- a/internal/cmd/workspaces.go +++ /dev/null @@ -1,1074 +0,0 @@ -package cmd - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "strings" - "time" - - "nhooyr.io/websocket" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/coderutil" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/clog" - "cdr.dev/coder-cli/pkg/tablewriter" - "cdr.dev/coder-cli/wsnet" - - "github.com/fatih/color" - "github.com/manifoldco/promptui" - "github.com/pion/ice/v2" - "github.com/pion/webrtc/v3" - "github.com/spf13/cobra" - "golang.org/x/xerrors" -) - -const defaultImgTag = "latest" - -func envCmd() *cobra.Command { - cmd := workspacesCmd() - cmd.Use = "envs" - cmd.Deprecated = "use \"workspaces\" instead" - cmd.Aliases = []string{} - return cmd -} - -func workspacesCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "workspaces", - Short: "Interact with Coder workspaces", - Long: "Perform operations on the Coder workspaces owned by the active user.", - Aliases: []string{"ws"}, - } - - cmd.AddCommand( - createWorkspaceCmd(), - editWorkspaceCmd(), - lsWorkspacesCommand(), - pingWorkspaceCommand(), - rebuildWorkspaceCommand(), - rmWorkspacesCmd(), - setPolicyTemplate(), - stopWorkspacesCmd(), - watchBuildLogCommand(), - workspaceFromConfigCmd(false), - workspaceFromConfigCmd(true), - ) - return cmd -} - -const ( - humanOutput = "human" - jsonOutput = "json" -) - -func lsWorkspacesCommand() *cobra.Command { - var ( - all bool - outputFmt string - user string - provider string - ) - - cmd := &cobra.Command{ - Use: "ls", - Short: "list all workspaces owned by the active user", - Long: "List all Coder workspaces owned by the active user.", - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - - var workspaces []coder.Workspace - switch { - case all: - workspaces, err = getAllWorkspaces(ctx, client) - case provider != "": - workspaces, err = getWorkspacesByProvider(ctx, client, provider, user) - default: - workspaces, err = getWorkspaces(ctx, client, user) - } - if err != nil { - return err - } - if len(workspaces) < 1 { - clog.LogInfo("no workspaces found") - workspaces = []coder.Workspace{} // ensures that json output still marshals - } - - switch outputFmt { - case humanOutput: - workspaces, err := coderutil.WorkspacesHumanTable(ctx, client, workspaces) - if err != nil { - return err - } - err = tablewriter.WriteTable(cmd.OutOrStdout(), len(workspaces), func(i int) interface{} { - return workspaces[i] - }) - if err != nil { - return xerrors.Errorf("write table: %w", err) - } - case jsonOutput: - err := json.NewEncoder(cmd.OutOrStdout()).Encode(workspaces) - if err != nil { - return xerrors.Errorf("write workspaces as JSON: %w", err) - } - default: - return xerrors.Errorf("unknown --output value %q", outputFmt) - } - return nil - }, - } - - cmd.Flags().BoolVar(&all, "all", false, "Get workspaces for all users (admin only)") - cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") - cmd.Flags().StringVarP(&outputFmt, "output", "o", humanOutput, "human | json") - cmd.Flags().StringVarP(&provider, "provider", "p", "", "Filter workspaces by a particular workspace provider name.") - - return cmd -} - -func pingWorkspaceCommand() *cobra.Command { - var ( - schemes []string - count int - ) - - cmd := &cobra.Command{ - Use: "ping ", - Short: "ping Coder workspaces by name", - Long: "ping Coder workspaces by name", - Example: `coder workspaces ping front-end-workspace`, - Args: xcobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - workspace, err := findWorkspace(ctx, client, args[0], coder.Me) - if err != nil { - return err - } - - iceSchemes := map[ice.SchemeType]interface{}{} - for _, rawScheme := range schemes { - scheme := ice.NewSchemeType(rawScheme) - if scheme == ice.Unknown { - return fmt.Errorf("scheme type %q not recognized", rawScheme) - } - iceSchemes[scheme] = nil - } - - pinger := &wsPinger{ - client: client, - workspace: workspace, - iceSchemes: iceSchemes, - } - - seq := 0 - ticker := time.NewTicker(time.Second) - for { - select { - case <-ticker.C: - err := pinger.ping(ctx) - if err != nil { - return err - } - seq++ - if count > 0 && seq >= count { - os.Exit(0) - } - case <-ctx.Done(): - return nil - } - } - }, - } - - cmd.Flags().StringSliceVarP(&schemes, "scheme", "s", []string{"stun", "stuns", "turn", "turns"}, "customize schemes to filter ice servers") - cmd.Flags().IntVarP(&count, "count", "c", 0, "stop after replies") - return cmd -} - -type wsPinger struct { - client coder.Client - workspace *coder.Workspace - dialer *wsnet.Dialer - iceSchemes map[ice.SchemeType]interface{} - tunneled bool -} - -func (*wsPinger) logFail(msg string) { - fmt.Printf("%s: %s\n", color.New(color.Bold, color.FgRed).Sprint("——"), msg) -} - -func (*wsPinger) logSuccess(timeStr, msg string) { - fmt.Printf("%s: %s\n", color.New(color.Bold, color.FgGreen).Sprint(timeStr), msg) -} - -// Only return fatal errors. -func (w *wsPinger) ping(ctx context.Context) error { - ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) - defer cancelFunc() - url := w.client.BaseURL() - - // If the dialer is nil we create a new! - // nolint:nestif - if w.dialer == nil { - servers, err := w.client.ICEServers(ctx) - if err != nil { - w.logFail(fmt.Sprintf("list ice servers: %s", err.Error())) - return nil - } - filteredServers := make([]webrtc.ICEServer, 0, len(servers)) - for _, server := range servers { - good := true - for _, rawURL := range server.URLs { - url, err := ice.ParseURL(rawURL) - if err != nil { - return fmt.Errorf("parse url %q: %w", rawURL, err) - } - if _, ok := w.iceSchemes[url.Scheme]; !ok { - good = false - } - } - if good { - filteredServers = append(filteredServers, server) - } - } - if len(filteredServers) == 0 { - schemes := make([]string, 0) - for scheme := range w.iceSchemes { - schemes = append(schemes, scheme.String()) - } - return fmt.Errorf("no ice servers match the schemes provided: %s", strings.Join(schemes, ",")) - } - workspace, err := w.client.WorkspaceByID(ctx, w.workspace.ID) - if err != nil { - return err - } - if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { - w.logFail(fmt.Sprintf("workspace is unreachable (status=%s)", workspace.LatestStat.ContainerStatus)) - return nil - } - connectStart := time.Now() - w.dialer, err = wsnet.DialWebsocket(ctx, wsnet.ConnectEndpoint(&url, w.workspace.ID, w.client.Token()), &wsnet.DialOptions{ - ICEServers: filteredServers, - TURNProxyAuthToken: w.client.Token(), - TURNRemoteProxyURL: &url, - TURNLocalProxyURL: &url, - }, &websocket.DialOptions{}) - if err != nil { - w.logFail(fmt.Sprintf("dial workspace: %s", err.Error())) - return nil - } - connectMS := float64(time.Since(connectStart).Microseconds()) / 1000 - - candidates, err := w.dialer.Candidates() - if err != nil { - return err - } - isRelaying := candidates.Local.Typ == webrtc.ICECandidateTypeRelay - w.tunneled = false - candidateURLs := []string{} - - for _, server := range filteredServers { - if server.Username == wsnet.TURNProxyICECandidate().Username { - candidateURLs = append(candidateURLs, fmt.Sprintf("turn:%s", url.Host)) - if !isRelaying { - continue - } - w.tunneled = true - continue - } - - candidateURLs = append(candidateURLs, server.URLs...) - } - - connectionText := "direct via STUN" - if isRelaying { - connectionText = "proxied via TURN" - } - if w.tunneled { - connectionText = fmt.Sprintf("proxied via %s", url.Host) - } - w.logSuccess("——", fmt.Sprintf( - "connected in %.2fms (%s) candidates=%s", - connectMS, - connectionText, - strings.Join(candidateURLs, ","), - )) - } - - pingStart := time.Now() - err := w.dialer.Ping(ctx) - if err != nil { - if errors.Is(err, io.EOF) { - w.dialer = nil - w.logFail("connection timed out") - return nil - } - if errors.Is(err, webrtc.ErrConnectionClosed) { - w.dialer = nil - w.logFail("webrtc connection is closed") - return nil - } - return fmt.Errorf("ping workspace: %w", err) - } - pingMS := float64(time.Since(pingStart).Microseconds()) / 1000 - connectionText := "you ↔ workspace" - if w.tunneled { - connectionText = fmt.Sprintf("you ↔ %s ↔ workspace", url.Host) - } - w.logSuccess(fmt.Sprintf("%.2fms", pingMS), connectionText) - return nil -} - -func stopWorkspacesCmd() *cobra.Command { - var user string - cmd := &cobra.Command{ - Use: "stop [...workspace_names]", - Short: "stop Coder workspaces by name", - Long: "Stop Coder workspaces by name", - Example: `coder workspaces stop front-end-workspace -coder workspaces stop front-end-workspace backend-workspace - -# stop all of your workspaces -coder workspaces ls -o json | jq -c '.[].name' | xargs coder workspaces stop - -# stop all workspaces for a given user -coder workspaces --user charlie@coder.com ls -o json \ - | jq -c '.[].name' \ - | xargs coder workspaces --user charlie@coder.com stop`, - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return xerrors.Errorf("new client: %w", err) - } - - egroup := clog.LoggedErrGroup() - for _, workspaceName := range args { - workspaceName := workspaceName - egroup.Go(func() error { - workspace, err := findWorkspace(ctx, client, workspaceName, user) - if err != nil { - return err - } - - if err = client.StopWorkspace(ctx, workspace.ID); err != nil { - return clog.Error(fmt.Sprintf("stop workspace %q", workspace.Name), - clog.Causef(err.Error()), clog.BlankLine, - clog.Hintf("current workspace status is %q", workspace.LatestStat.ContainerStatus), - ) - } - clog.LogSuccess(fmt.Sprintf("successfully stopped workspace %q", workspaceName)) - return nil - }) - } - - return egroup.Wait() - }, - } - cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") - return cmd -} - -func createWorkspaceCmd() *cobra.Command { - var ( - org string - cpu float32 - memory float32 - disk int - gpus int - img string - tag string - follow bool - useCVM bool - providerName string - enableAutostart bool - forUser string // Optional - ) - - cmd := &cobra.Command{ - Use: "create [workspace_name]", - Short: "create a new workspace.", - Args: xcobra.ExactArgs(1), - Long: "Create a new Coder workspace.", - Example: `# create a new workspace using default resource amounts -coder workspaces create my-new-workspace --image ubuntu -coder workspaces create my-new-powerful-workspace --cpu 12 --disk 100 --memory 16 --image ubuntu`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - if img == "" { - return xerrors.New("image unset") - } - - client, err := newClient(ctx, true) - if err != nil { - return err - } - - multiOrgMember, err := isMultiOrgMember(ctx, client, coder.Me) - if err != nil { - return err - } - - if multiOrgMember && org == "" { - return xerrors.New("org is required for multi-org members") - } - importedImg, err := findImg(ctx, client, findImgConf{ - email: coder.Me, - imgName: img, - orgName: org, - }) - if err != nil { - return err - } - - var provider *coder.KubernetesProvider - if providerName == "" { - provider, err = coderutil.DefaultWorkspaceProvider(ctx, client) - if err != nil { - return xerrors.Errorf("default workspace provider: %w", err) - } - } else { - provider, err = coderutil.ProviderByName(ctx, client, providerName) - if err != nil { - return xerrors.Errorf("provider by name: %w", err) - } - } - - var forEmail string - if forUser != "" && forUser != coder.Me { - // Making a workspace for another user, do they exist? - u, err := client.UserByEmail(ctx, forUser) - if err != nil { - // Try by ID? - u, err = client.UserByID(ctx, forUser) - if err != nil { - return xerrors.Errorf("the user %q was not found: %w", forUser, err) - } - } - forUser = u.ID - forEmail = u.Email - } else if forUser == coder.Me { - forUser = "" // coder.Me means it's not for someone else, set blank - } - - // ExactArgs(1) ensures our name value can't panic on an out of bounds. - createReq := &coder.CreateWorkspaceRequest{ - Name: args[0], - ImageID: importedImg.ID, - OrgID: importedImg.OrganizationID, - ImageTag: tag, - CPUCores: cpu, - MemoryGB: memory, - DiskGB: disk, - GPUs: gpus, - UseContainerVM: useCVM, - ResourcePoolID: provider.ID, - Namespace: provider.DefaultNamespace, - EnableAutoStart: enableAutostart, - ForUserID: forUser, - } - - // if any of these defaulted to their zero value we provision - // the create request with the imported image defaults instead. - if createReq.CPUCores == 0 { - createReq.CPUCores = importedImg.DefaultCPUCores - } - if createReq.MemoryGB == 0 { - createReq.MemoryGB = importedImg.DefaultMemoryGB - } - if createReq.DiskGB == 0 { - createReq.DiskGB = importedImg.DefaultDiskGB - } - - workspace, err := client.CreateWorkspace(ctx, *createReq) - if err != nil { - return xerrors.Errorf("create workspace: %w", err) - } - - if follow { - clog.LogSuccess("creating workspace...") - if err := trailBuildLogs(ctx, client, workspace.ID); err != nil { - return err - } - return nil - } - - extraFlags := "" - if forEmail != coder.Me && forEmail != "" { - extraFlags = " --user " + forEmail - } - clog.LogSuccess("creating workspace...", - clog.BlankLine, - clog.Tipf(`run "coder workspaces watch-build %s%s" to trail the build logs`, workspace.Name, extraFlags), - ) - return nil - }, - } - cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the workspace should be created under.") - cmd.Flags().StringVarP(&tag, "tag", "t", defaultImgTag, "tag of the image the workspace will be based off of.") - cmd.Flags().Float32VarP(&cpu, "cpu", "c", 0, "number of cpu cores the workspace should be provisioned with.") - cmd.Flags().Float32VarP(&memory, "memory", "m", 0, "GB of RAM a workspace should be provisioned with.") - cmd.Flags().IntVarP(&disk, "disk", "d", 0, "GB of disk storage a workspace should be provisioned with.") - cmd.Flags().IntVarP(&gpus, "gpus", "g", 0, "number GPUs a workspace should be provisioned with.") - cmd.Flags().StringVarP(&img, "image", "i", "", "name of the image to base the workspace off of.") - cmd.Flags().StringVar(&providerName, "provider", "", "name of Workspace Provider with which to create the workspace") - cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild") - cmd.Flags().BoolVar(&useCVM, "container-based-vm", false, "deploy the workspace as a Container-based VM") - cmd.Flags().BoolVar(&enableAutostart, "enable-autostart", false, "automatically start this workspace at your preferred time.") - cmd.Flags().StringVar(&forUser, "user", coder.Me, "Specify the user whose resources to target. This flag can only be used by admins and managers. Input an email or user id.") - _ = cmd.MarkFlagRequired("image") - return cmd -} - -// selectOrg finds the organization in the list or returns the default organization -// if the needle isn't found. -func selectOrg(needle string, haystack []coder.Organization) (*coder.Organization, error) { - var userOrg *coder.Organization - for i := range haystack { - // Look for org by name - if haystack[i].Name == needle { - userOrg = &haystack[i] - break - } - // Or use default if the provided is blank - if needle == "" && haystack[i].Default { - userOrg = &haystack[i] - break - } - } - - if userOrg == nil { - if needle != "" { - return nil, xerrors.Errorf("Unable to locate org '%s'", needle) - } - return nil, xerrors.Errorf("Unable to locate a default organization for the user") - } - return userOrg, nil -} - -// workspaceFromConfigCmd will return a create or an update workspace for a template'd workspace. -// The code for create/update is nearly identical. -// If `update` is true, the update command is returned. If false, the create command. -func workspaceFromConfigCmd(update bool) *cobra.Command { - var ( - ref string - repo string - follow bool - filepath string - org string - providerName string - workspaceName string - ) - - run := func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - // Update requires the env name, and the name should be the first argument. - if update { - workspaceName = args[0] - } else if workspaceName == "" { - // Create takes the name as a flag, and it must be set - return clog.Error("Must provide a workspace name.", - clog.BlankLine, - clog.Tipf("Use --name= to name your workspace"), - ) - } - - client, err := newClient(ctx, true) - if err != nil { - return err - } - - orgs, err := getUserOrgs(ctx, client, coder.Me) - if err != nil { - return err - } - - multiOrgMember := len(orgs) > 1 - if multiOrgMember && org == "" { - return xerrors.New("org is required for multi-org members") - } - - // This is the env to be updated/created - var workspace *coder.Workspace - - // OrgID is the org where the template and env should be created. - // If we are updating an env, use the orgID from the workspace. - var orgID string - if update { - workspace, err = findWorkspace(ctx, client, workspaceName, coder.Me) - if err != nil { - return handleAPIError(err) - } - orgID = workspace.OrganizationID - } else { - var userOrg *coder.Organization - // Select org in list or use default - userOrg, err := selectOrg(org, orgs) - if err != nil { - return err - } - - orgID = userOrg.ID - } - - if filepath == "" && ref == "" && repo == "" { - return clog.Error("Must specify a configuration source", - "A template source is either sourced from a local file (-f) or from a git repository (--repo-url and --ref)", - ) - } - - var rd io.Reader - if filepath != "" { - b, err := ioutil.ReadFile(filepath) - if err != nil { - return xerrors.Errorf("read local file: %w", err) - } - rd = bytes.NewReader(b) - } - - req := coder.ParseTemplateRequest{ - RepoURL: repo, - Ref: ref, - Local: rd, - OrgID: orgID, - Filepath: ".coder/coder.yaml", - } - - version, err := client.ParseTemplate(ctx, req) - if err != nil { - return handleAPIError(err) - } - - provider, err := coderutil.DefaultWorkspaceProvider(ctx, client) - if err != nil { - return xerrors.Errorf("default workspace provider: %w", err) - } - - if update { - err = client.EditWorkspace(ctx, workspace.ID, coder.UpdateWorkspaceReq{ - TemplateID: &version.TemplateID, - }) - } else { - workspace, err = client.CreateWorkspace(ctx, coder.CreateWorkspaceRequest{ - OrgID: orgID, - TemplateID: version.TemplateID, - ResourcePoolID: provider.ID, - Namespace: provider.DefaultNamespace, - Name: workspaceName, - }) - } - if err != nil { - return handleAPIError(err) - } - - if follow { - clog.LogSuccess("creating workspace...") - if err := trailBuildLogs(ctx, client, workspace.ID); err != nil { - return err - } - return nil - } - - clog.LogSuccess("creating workspace...", - clog.BlankLine, - clog.Tipf(`run "coder workspaces watch-build %s" to see build logs`, workspace.Name), - ) - return nil - } - - var cmd *cobra.Command - if update { - cmd = &cobra.Command{ - Use: "edit-from-config", - Short: "change the template a workspace is tracking", - Long: "Edit an existing Coder workspace using a workspace template.", - Args: cobra.ExactArgs(1), - Example: `# edit a new workspace from git repository -coder workspaces edit-from-config dev-env --repo-url https://github.com/cdr/m --ref my-branch -coder workspaces edit-from-config dev-env --filepath coder.yaml`, - RunE: run, - } - } else { - cmd = &cobra.Command{ - Use: "create-from-config", - Short: "create a new workspace from a template", - Long: "Create a new Coder workspace using a workspace template.", - Example: `# create a new workspace from git repository -coder workspaces create-from-config --name="dev-env" --repo-url https://github.com/cdr/m --ref my-branch -coder workspaces create-from-config --name="dev-env" --filepath coder.yaml`, - RunE: run, - } - cmd.Flags().StringVar(&providerName, "provider", "", "name of Workspace Provider with which to create the workspace") - cmd.Flags().StringVar(&workspaceName, "name", "", "name of the workspace to be created") - cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the workspace should be created under.") - // Ref and repo-url can only be used for create - cmd.Flags().StringVarP(&ref, "ref", "", "master", "git reference to pull template from. May be a branch, tag, or commit hash.") - cmd.Flags().StringVarP(&repo, "repo-url", "r", "", "URL of the git repository to pull the config from. Config file must live in '.coder/coder.yaml'.") - } - - cmd.Flags().StringVarP(&filepath, "filepath", "f", "", "path to local template file.") - cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild") - return cmd -} - -func editWorkspaceCmd() *cobra.Command { - var ( - org string - img string - tag string - cpu float32 - memory float32 - disk int - gpus int - follow bool - user string - force bool - ) - - cmd := &cobra.Command{ - Use: "edit", - Short: "edit an existing workspace and initiate a rebuild.", - Args: xcobra.ExactArgs(1), - Long: "Edit an existing workspace and initate a rebuild.", - Example: `coder workspaces edit back-end-workspace --cpu 4 - -coder workspaces edit back-end-workspace --disk 20`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - - workspaceName := args[0] - - workspace, err := findWorkspace(ctx, client, workspaceName, user) - if err != nil { - return err - } - - multiOrgMember, err := isMultiOrgMember(ctx, client, user) - if err != nil { - return err - } - - // if the user belongs to multiple organizations we need them to specify which one. - if multiOrgMember && org == "" { - return xerrors.New("org is required for multi-org members") - } - - req, err := buildUpdateReq(ctx, client, updateConf{ - cpu: cpu, - memGB: memory, - diskGB: disk, - gpus: gpus, - workspace: workspace, - user: user, - image: img, - imageTag: tag, - orgName: org, - }) - if err != nil { - return err - } - - if !force && workspace.LatestStat.ContainerStatus == coder.WorkspaceOn { - _, err = (&promptui.Prompt{ - Label: fmt.Sprintf("Rebuild workspace %q? (will destroy any work outside of your home directory)", workspace.Name), - IsConfirm: true, - }).Run() - if err != nil { - return clog.Fatal( - "failed to confirm prompt", clog.BlankLine, - clog.Tipf(`use "--force" to rebuild without a confirmation prompt`), - ) - } - } - - if err := client.EditWorkspace(ctx, workspace.ID, *req); err != nil { - return xerrors.Errorf("failed to apply changes to workspace %q: %w", workspaceName, err) - } - - if follow { - clog.LogSuccess("applied changes to the workspace, rebuilding...") - if err := trailBuildLogs(ctx, client, workspace.ID); err != nil { - return err - } - return nil - } - - clog.LogSuccess("applied changes to the workspace, rebuilding...", - clog.BlankLine, - clog.Tipf(`run "coder workspaces watch-build %s" to trail the build logs`, workspaceName), - ) - return nil - }, - } - cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the workspace should be created under.") - cmd.Flags().StringVarP(&img, "image", "i", "", "name of the image you want the workspace to be based off of.") - cmd.Flags().StringVarP(&tag, "tag", "t", "latest", "image tag of the image you want to base the workspace off of.") - cmd.Flags().Float32VarP(&cpu, "cpu", "c", 0, "The number of cpu cores the workspace should be provisioned with.") - cmd.Flags().Float32VarP(&memory, "memory", "m", 0, "The amount of RAM a workspace should be provisioned with.") - cmd.Flags().IntVarP(&disk, "disk", "d", 0, "The amount of disk storage a workspace should be provisioned with.") - cmd.Flags().IntVarP(&gpus, "gpu", "g", 0, "The amount of disk storage to provision the workspace with.") - cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild") - cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") - cmd.Flags().BoolVar(&force, "force", false, "force rebuild without showing a confirmation prompt") - return cmd -} - -func rmWorkspacesCmd() *cobra.Command { - var ( - force bool - user string - ) - - cmd := &cobra.Command{ - Use: "rm [...workspace_names]", - Short: "remove Coder workspaces by name", - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - if !force { - confirm := promptui.Prompt{ - Label: fmt.Sprintf("Delete workspaces %q? (all data will be lost)", args), - IsConfirm: true, - } - if _, err := confirm.Run(); err != nil { - return clog.Fatal( - "failed to confirm deletion", clog.BlankLine, - clog.Tipf(`use "--force" to rebuild without a confirmation prompt`), - ) - } - } - - egroup := clog.LoggedErrGroup() - for _, workspaceName := range args { - workspaceName := workspaceName - egroup.Go(func() error { - workspace, err := findWorkspace(ctx, client, workspaceName, user) - if err != nil { - return err - } - if err = client.DeleteWorkspace(ctx, workspace.ID); err != nil { - return clog.Error( - fmt.Sprintf(`failed to delete workspace "%s"`, workspace.Name), - clog.Causef(err.Error()), - ) - } - clog.LogSuccess(fmt.Sprintf("deleted workspace %q", workspace.Name)) - return nil - }) - } - return egroup.Wait() - }, - } - cmd.Flags().BoolVarP(&force, "force", "f", false, "force remove the specified workspaces without prompting first") - cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") - return cmd -} - -type updateConf struct { - cpu float32 - memGB float32 - diskGB int - gpus int - workspace *coder.Workspace - user string - image string - imageTag string - orgName string -} - -func buildUpdateReq(ctx context.Context, client coder.Client, conf updateConf) (*coder.UpdateWorkspaceReq, error) { - var ( - updateReq coder.UpdateWorkspaceReq - defaultCPUCores float32 - defaultMemGB float32 - defaultDiskGB int - ) - - // If this is not empty it means the user is requesting to change the workspace image. - if conf.image != "" { - importedImg, err := findImg(ctx, client, findImgConf{ - email: conf.user, - imgName: conf.image, - orgName: conf.orgName, - }) - if err != nil { - return nil, err - } - - // If the user passes an image arg of the image that - // the workspace is already using, it was most likely a mistake. - if conf.image != importedImg.Repository { - return nil, xerrors.Errorf("workspace is already using image %q", conf.image) - } - - // Since the workspace image is being changed, - // the resource amount defaults should be changed to - // reflect that of the default resource amounts of the new image. - defaultCPUCores = importedImg.DefaultCPUCores - defaultMemGB = importedImg.DefaultMemoryGB - defaultDiskGB = importedImg.DefaultDiskGB - updateReq.ImageID = &importedImg.ID - } else { - // if the workspace image is not being changed, the default - // resource amounts should reflect the default resource amounts - // of the image the workspace is already using. - defaultCPUCores = conf.workspace.CPUCores - defaultMemGB = conf.workspace.MemoryGB - defaultDiskGB = conf.workspace.DiskGB - updateReq.ImageID = &conf.workspace.ImageID - } - - // The following logic checks to see if the user specified - // any resource amounts for the workspace that need to be changed. - // If they did not, then we will get the zero value back - // and should set the resource amount to the default. - - if conf.cpu == 0 { - updateReq.CPUCores = &defaultCPUCores - } else { - updateReq.CPUCores = &conf.cpu - } - - if conf.memGB == 0 { - updateReq.MemoryGB = &defaultMemGB - } else { - updateReq.MemoryGB = &conf.memGB - } - - if conf.diskGB == 0 { - updateReq.DiskGB = &defaultDiskGB - } else { - updateReq.DiskGB = &conf.diskGB - } - - // Workspace disks can not be shrink so we have to overwrite this - // if the user accidentally requests it or if the default diskGB value for a - // newly requested image is smaller than the current amount the workspace is using. - if *updateReq.DiskGB < conf.workspace.DiskGB { - clog.LogWarn("disk can not be shrunk", - fmt.Sprintf("keeping workspace disk at %d GB", conf.workspace.DiskGB), - ) - updateReq.DiskGB = &conf.workspace.DiskGB - } - - if conf.gpus != 0 { - updateReq.GPUs = &conf.gpus - } - - if conf.imageTag == "" { - // We're forced to make an alloc here because untyped string consts are not addressable. - // i.e. updateReq.ImageTag = &defaultImgTag results in : - // invalid operation: cannot take address of defaultImgTag (untyped string constant "latest") - imgTag := defaultImgTag - updateReq.ImageTag = &imgTag - } else { - updateReq.ImageTag = &conf.imageTag - } - return &updateReq, nil -} - -func setPolicyTemplate() *cobra.Command { - var ( - ref string - repo string - filepath string - dryRun bool - defaultTemplate bool - scope string - ) - - cmd := &cobra.Command{ - Use: "policy-template", - Short: "Set workspace policy template", - Long: "Set workspace policy template or restore to default configuration. This feature is for site admins only.", - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - - if scope != coder.TemplateScopeSite { - return clog.Error("Invalid 'scope' value", "Valid scope values: site") - } - - if filepath == "" && !defaultTemplate { - return clog.Error("Missing required parameter --filepath or --default", "Must specify a template to set") - } - - templateID := "" - if filepath != "" { - var rd io.Reader - b, err := ioutil.ReadFile(filepath) - if err != nil { - return xerrors.Errorf("read local file: %w", err) - } - rd = bytes.NewReader(b) - - req := coder.ParseTemplateRequest{ - RepoURL: repo, - Ref: ref, - Local: rd, - OrgID: coder.SkipTemplateOrg, - Filepath: ".coder/coder.yaml", - } - - version, err := client.ParseTemplate(ctx, req) - if err != nil { - return handleAPIError(err) - } - templateID = version.TemplateID - } - - resp, err := client.SetPolicyTemplate(ctx, templateID, coder.TemplateScope(scope), dryRun) - if err != nil { - return handleAPIError(err) - } - - for _, mc := range resp.MergeConflicts { - workspace, err := client.WorkspaceByID(ctx, mc.WorkspaceID) - if err != nil { - fmt.Printf("Workspace %q:\n", mc.WorkspaceID) - } else { - fmt.Printf("Workspace %q in organization %q:\n", workspace.Name, workspace.OrganizationID) - } - - fmt.Println(mc.String()) - } - - fmt.Println("Summary:") - fmt.Println(coder.WorkspaceTemplateMergeConflicts(resp.MergeConflicts).Summary()) - - return nil - }, - } - cmd.Flags().BoolVarP(&dryRun, "dry-run", "", false, "skip setting policy template, but view errors/warnings about how this policy template would impact existing workspaces") - cmd.Flags().StringVarP(&filepath, "filepath", "f", "", "full path to local policy template file.") - cmd.Flags().StringVar(&scope, "scope", "site", "scope of impact for the policy template. Supported values: site") - cmd.Flags().BoolVar(&defaultTemplate, "default", false, "Restore policy template to default configuration") - return cmd -} diff --git a/internal/cmd/workspaces_test.go b/internal/cmd/workspaces_test.go deleted file mode 100644 index 89e5fd08..00000000 --- a/internal/cmd/workspaces_test.go +++ /dev/null @@ -1,235 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "math" - "math/rand" - "os" - "testing" - "time" - - "cdr.dev/slog" - "cdr.dev/slog/sloggers/slogtest" - "cdr.dev/slog/sloggers/slogtest/assert" - "github.com/google/go-cmp/cmp" - - "cdr.dev/coder-cli/coder-sdk" -) - -func Test_workspaces_ls(t *testing.T) { - skipIfNoAuth(t) - res := execute(t, nil, "workspaces", "ls") - res.success(t) - - res = execute(t, nil, "workspaces", "ls", "--output=json") - res.success(t) - - var workspaces []coder.Workspace - res.stdoutUnmarshals(t, &workspaces) -} - -func Test_workspaces_ls_all(t *testing.T) { - skipIfNoAuth(t) - for _, test := range []struct { - name string - command []string - assert func(r result) - }{ - { - name: "simple list", - command: []string{"workspaces", "ls", "--all"}, - assert: func(r result) { r.success(t) }, - }, - { - name: "list as json", - command: []string{"workspaces", "ls", "--all", "--output", "json"}, - assert: func(r result) { - var workspaces []coder.Workspace - r.stdoutUnmarshals(t, &workspaces) - }, - }, - } { - test := test - t.Run(test.name, func(t *testing.T) { - test.assert(execute(t, nil, test.command...)) - }) - } -} - -func Test_workspaces_ls_by_provider(t *testing.T) { - skipIfNoAuth(t) - for _, test := range []struct { - name string - command []string - assert func(r result) - }{ - { - name: "simple list", - command: []string{"workspaces", "ls", "--provider", "built-in"}, - assert: func(r result) { r.success(t) }, - }, - { - name: "list as json", - command: []string{"workspaces", "ls", "--provider", "built-in", "--output", "json"}, - assert: func(r result) { - var workspaces []coder.Workspace - r.stdoutUnmarshals(t, &workspaces) - }, - }, - } { - test := test - t.Run(test.name, func(t *testing.T) { - test.assert(execute(t, nil, test.command...)) - }) - } -} - -func Test_workspace_create(t *testing.T) { - skipIfNoAuth(t) - ctx := context.Background() - - // Minimum args not received. - res := execute(t, nil, "workspaces", "create") - res.error(t) - res.stderrContains(t, "accepts 1 arg(s), received 0") - - // Successfully output help. - res = execute(t, nil, "workspaces", "create", "--help") - res.success(t) - res.stdoutContains(t, "Create a new Coder workspace.") - - // Image unset - res = execute(t, nil, "workspaces", "create", "test-workspace") - res.error(t) - res.stderrContains(t, "fatal: required flag(s) \"image\" not set") - - // Image not imported - res = execute(t, nil, "workspaces", "create", "test-workspace", "--image=doestexist") - res.error(t) - res.stderrContains(t, "fatal: image not found - did you forget to import this image?") - - ensureImageImported(ctx, t, testCoderClient, "codercom/enterprise-base", "ubuntu") - - name := randString(10) - cpu := 2.3 - - // attempt to remove the workspace on cleanup - t.Cleanup(func() { _ = execute(t, nil, "workspaces", "rm", name, "--force") }) - - res = execute(t, nil, "workspaces", "create", name, "--image=codercom/enterprise-base", "--tag=ubuntu", fmt.Sprintf("--cpu=%f", cpu)) - res.success(t) - - res = execute(t, nil, "workspaces", "ls") - res.success(t) - res.stdoutContains(t, name) - - var workspaces []coder.Workspace - res = execute(t, nil, "workspaces", "ls", "--output=json") - res.success(t) - res.stdoutUnmarshals(t, &workspaces) - workspace := assertWorkspace(t, name, workspaces) - assert.Equal(t, "workspace cpu", cpu, float64(workspace.CPUCores), floatComparer) - - res = execute(t, nil, "workspaces", "watch-build", name) - res.success(t) - - // edit the CPU of the workspace - cpu = 2.1 - res = execute(t, nil, "workspaces", "edit", name, "--image=codercom/enterprise-base", "--tag=ubuntu", fmt.Sprintf("--cpu=%f", cpu), "--follow", "--force") - res.success(t) - - // assert that the CPU actually did change after edit - res = execute(t, nil, "workspaces", "ls", "--output=json") - res.success(t) - res.stdoutUnmarshals(t, &workspaces) - workspace = assertWorkspace(t, name, workspaces) - assert.Equal(t, "workspace cpu", cpu, float64(workspace.CPUCores), floatComparer) - - res = execute(t, nil, "workspaces", "rm", name, "--force") - res.success(t) -} - -func assertWorkspace(t *testing.T, name string, workspaces []coder.Workspace) *coder.Workspace { - for _, e := range workspaces { - if name == e.Name { - return &e - } - } - slogtest.Fatal(t, "workspace not found", slog.F("name", name), slog.F("workspaces", workspaces)) - return nil -} - -var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) - -//nolint:unparam -func randString(length int) string { - const charset = "abcdefghijklmnopqrstuvwxyz" - b := make([]byte, length) - for i := range b { - b[i] = charset[seededRand.Intn(len(charset))] - } - return string(b) -} - -var floatComparer = cmp.Comparer(func(x, y float64) bool { - delta := math.Abs(x - y) - mean := math.Abs(x+y) / 2.0 - return delta/mean < 0.001 -}) - -// this is a stopgap until we have support for a `coder images` subcommand -// until then, we can use the coder.Client to ensure our integration tests -// work on fresh deployments. -func ensureImageImported(ctx context.Context, t *testing.T, client coder.Client, img, tag string) { - orgs, err := client.Organizations(ctx) - assert.Success(t, "get orgs", err) - - var org *coder.Organization -search: - for _, o := range orgs { - for _, m := range o.Members { - if m.Email == os.Getenv("CODER_EMAIL") { - o := o - org = &o - break search - } - } - } - if org == nil { - slogtest.Fatal(t, "failed to find org of current user") - return // help the linter out a bit - } - - registries, err := client.Registries(ctx, org.ID) - assert.Success(t, "get registries", err) - - var dockerhubID string - for _, r := range registries { - if r.Registry == "index.docker.io" { - dockerhubID = r.ID - } - } - assert.True(t, "docker hub registry found", dockerhubID != "") - - imgs, err := client.OrganizationImages(ctx, org.ID) - assert.Success(t, "get org images", err) - found := false - for _, i := range imgs { - if i.Repository == img { - found = true - } - } - if !found { - // ignore this error for now as it causes a race with other parallel tests - _, _ = client.ImportImage(ctx, coder.ImportImageReq{ - RegistryID: &dockerhubID, - OrgID: org.ID, - Repository: img, - Tag: tag, - DefaultCPUCores: 2.5, - DefaultDiskGB: 22, - DefaultMemoryGB: 3, - }) - } -} diff --git a/internal/coderutil/doc.go b/internal/coderutil/doc.go deleted file mode 100644 index 5a7d8e14..00000000 --- a/internal/coderutil/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package coderutil providers utilities for high-level operations on coder-sdk entities. -package coderutil diff --git a/internal/coderutil/provider.go b/internal/coderutil/provider.go deleted file mode 100644 index 5364add8..00000000 --- a/internal/coderutil/provider.go +++ /dev/null @@ -1,21 +0,0 @@ -package coderutil - -import ( - "context" - - "cdr.dev/coder-cli/coder-sdk" -) - -// ProviderByName searches linearly for a workspace provider by its name. -func ProviderByName(ctx context.Context, client coder.Client, name string) (*coder.KubernetesProvider, error) { - providers, err := client.WorkspaceProviders(ctx) - if err != nil { - return nil, err - } - for _, p := range providers.Kubernetes { - if p.Name == name { - return &p, nil - } - } - return nil, coder.ErrNotFound -} diff --git a/internal/coderutil/workspace.go b/internal/coderutil/workspace.go deleted file mode 100644 index 8dba71cd..00000000 --- a/internal/coderutil/workspace.go +++ /dev/null @@ -1,209 +0,0 @@ -package coderutil - -import ( - "context" - "fmt" - "net/url" - "sync" - - "golang.org/x/xerrors" - "nhooyr.io/websocket" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/clog" -) - -// DialWorkspaceWsep dials the executor endpoint using the https://github.com/cdr/wsep message protocol. -// The proper workspace provider envproxy access URL is used. -func DialWorkspaceWsep(ctx context.Context, client coder.Client, workspace *coder.Workspace) (*websocket.Conn, error) { - workspaceProvider, err := client.WorkspaceProviderByID(ctx, workspace.ResourcePoolID) - if err != nil { - return nil, xerrors.Errorf("get workspace workspace provider: %w", err) - } - accessURL, err := url.Parse(workspaceProvider.EnvproxyAccessURL) - if err != nil { - return nil, xerrors.Errorf("invalid workspace provider envproxy access url: %w", err) - } - - conn, err := client.DialWsep(ctx, accessURL, workspace.ID) - if err != nil { - return nil, xerrors.Errorf("dial websocket: %w", err) - } - return conn, nil -} - -// WorkspaceWithWorkspaceProvider composes an Workspace entity with its associated WorkspaceProvider. -type WorkspaceWithWorkspaceProvider struct { - Workspace coder.Workspace - WorkspaceProvider coder.KubernetesProvider -} - -// WorkspacesWithProvider performs the composition of each Workspace with its associated WorkspaceProvider. -func WorkspacesWithProvider(ctx context.Context, client coder.Client, workspaces []coder.Workspace) ([]WorkspaceWithWorkspaceProvider, error) { - pooledWorkspaces := make([]WorkspaceWithWorkspaceProvider, 0, len(workspaces)) - providers, err := client.WorkspaceProviders(ctx) - if err != nil { - return nil, err - } - providerMap := make(map[string]coder.KubernetesProvider, len(providers.Kubernetes)) - for _, p := range providers.Kubernetes { - providerMap[p.ID] = p - } - for _, e := range workspaces { - workspaceProvider, ok := providerMap[e.ResourcePoolID] - if !ok { - return nil, xerrors.Errorf("fetch workspace workspace provider: %w", coder.ErrNotFound) - } - pooledWorkspaces = append(pooledWorkspaces, WorkspaceWithWorkspaceProvider{ - Workspace: e, - WorkspaceProvider: workspaceProvider, - }) - } - return pooledWorkspaces, nil -} - -// DefaultWorkspaceProvider returns the default provider with which to create workspaces. -func DefaultWorkspaceProvider(ctx context.Context, c coder.Client) (*coder.KubernetesProvider, error) { - provider, err := c.WorkspaceProviders(ctx) - if err != nil { - return nil, err - } - for _, p := range provider.Kubernetes { - if p.BuiltIn { - return &p, nil - } - } - return nil, coder.ErrNotFound -} - -// WorkspaceTable defines an Workspace-like structure with associated entities composed in a human -// readable form. -type WorkspaceTable struct { - Name string `table:"Name" json:"name"` - Image string `table:"Image" json:"image"` - CPU float32 `table:"vCPU" json:"cpu"` - MemoryGB float32 `table:"MemoryGB" json:"memory_gb"` - DiskGB int `table:"DiskGB" json:"disk_gb"` - Status string `table:"Status" json:"status"` - Provider string `table:"Provider" json:"provider"` - CVM bool `table:"CVM" json:"cvm"` - Username string `table:"Username" json:"username"` -} - -// WorkspacesHumanTable performs the composition of each Workspace with its associated ProviderName and ImageRepo. -func WorkspacesHumanTable(ctx context.Context, client coder.Client, workspaces []coder.Workspace) ([]WorkspaceTable, error) { - imageMap, err := MakeImageMap(ctx, client, workspaces) - if err != nil { - return nil, err - } - - userMap, err := MakeUserMap(ctx, client, workspaces) - if err != nil { - return nil, err - } - - pooledWorkspaces := make([]WorkspaceTable, 0, len(workspaces)) - providers, err := client.WorkspaceProviders(ctx) - if err != nil { - return nil, err - } - providerMap := make(map[string]coder.KubernetesProvider, len(providers.Kubernetes)) - for _, p := range providers.Kubernetes { - providerMap[p.ID] = p - } - for _, ws := range workspaces { - workspaceProvider, ok := providerMap[ws.ResourcePoolID] - if !ok { - return nil, xerrors.Errorf("fetch workspace workspace provider: %w", coder.ErrNotFound) - } - pooledWorkspaces = append(pooledWorkspaces, WorkspaceTable{ - Name: ws.Name, - Image: fmt.Sprintf("%s:%s", imageMap[ws.ImageID].Repository, ws.ImageTag), - CPU: ws.CPUCores, - MemoryGB: ws.MemoryGB, - DiskGB: ws.DiskGB, - Status: string(ws.LatestStat.ContainerStatus), - Provider: workspaceProvider.Name, - CVM: ws.UseContainerVM, - Username: userMap[ws.UserID].Username, - }) - } - return pooledWorkspaces, nil -} - -func MakeUserMap(ctx context.Context, client coder.Client, workspaces []coder.Workspace) (map[string]*coder.User, error) { - var ( - mu sync.Mutex - egroup = clog.LoggedErrGroup() - ) - - userMap := map[string]*coder.User{} - - // Iterate over all the workspaces to get a list of unique User IDs. - for _, ws := range workspaces { - userMap[ws.UserID] = nil - } - - fetchIds := make([]string, 0, len(userMap)) - for id := range userMap { - fetchIds = append(fetchIds, id) - } - - for _, id := range fetchIds { - id := id - egroup.Go(func() error { - user, err := client.UserByID(ctx, id) - if err != nil { - return xerrors.Errorf("get user by id: %w", err) - } - mu.Lock() - defer mu.Unlock() - - userMap[id] = user - return nil - }) - } - - if err := egroup.Wait(); err != nil { - return nil, xerrors.Errorf("fetch all workspace users: %w", err) - } - - return userMap, nil -} - -// MakeImageMap fetches all image entities specified in the slice of workspaces, then places them into an ID map. -func MakeImageMap(ctx context.Context, client coder.Client, workspaces []coder.Workspace) (map[string]*coder.Image, error) { - var ( - mu sync.Mutex - egroup = clog.LoggedErrGroup() - ) - imageMap := make(map[string]*coder.Image) - for _, e := range workspaces { - // put all the image IDs into a map to remove duplicates - imageMap[e.ImageID] = nil - } - ids := make([]string, 0, len(imageMap)) - for id := range imageMap { - // put the deduplicated back into a slice - // so we can write to the map while iterating - ids = append(ids, id) - } - for _, id := range ids { - id := id - egroup.Go(func() error { - img, err := client.ImageByID(ctx, id) - if err != nil { - return err - } - mu.Lock() - defer mu.Unlock() - imageMap[id] = img - - return nil - }) - } - if err := egroup.Wait(); err != nil { - return nil, err - } - return imageMap, nil -} diff --git a/internal/config/dir.go b/internal/config/dir.go deleted file mode 100644 index aff69fca..00000000 --- a/internal/config/dir.go +++ /dev/null @@ -1,52 +0,0 @@ -package config - -import ( - "io/ioutil" - "os" - "path/filepath" - - "github.com/kirsle/configdir" -) - -var configRoot = configdir.LocalConfig("coder") - -// SetRoot overrides the package-level config root configuration. -func SetRoot(root string) { - configRoot = root -} - -// open opens a file in the configuration directory, -// creating all intermediate directories. -func open(path string, flag int, mode os.FileMode) (*os.File, error) { - path = filepath.Join(configRoot, path) - - err := os.MkdirAll(filepath.Dir(path), 0750) - if err != nil { - return nil, err - } - - return os.OpenFile(path, flag, mode) -} - -func write(path string, mode os.FileMode, dat []byte) error { - fi, err := open(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, mode) - if err != nil { - return err - } - defer fi.Close() - _, err = fi.Write(dat) - return err -} - -func read(path string) ([]byte, error) { - fi, err := open(path, os.O_RDONLY, 0) - if err != nil { - return nil, err - } - defer fi.Close() - return ioutil.ReadAll(fi) -} - -func rm(path string) error { - return os.Remove(filepath.Join(configRoot, path)) -} diff --git a/internal/config/doc.go b/internal/config/doc.go deleted file mode 100644 index 69ff5641..00000000 --- a/internal/config/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package config provides facilities for working with the local configuration -// directory. -package config diff --git a/internal/config/file.go b/internal/config/file.go deleted file mode 100644 index 8ef1a910..00000000 --- a/internal/config/file.go +++ /dev/null @@ -1,26 +0,0 @@ -package config - -// File provides convenience methods for interacting with *os.File. -type File string - -// Delete deletes the file. -func (f File) Delete() error { - return rm(string(f)) -} - -// Write writes the string to the file. -func (f File) Write(s string) error { - return write(string(f), 0600, []byte(s)) -} - -// Read reads the file to a string. -func (f File) Read() (string, error) { - byt, err := read(string(f)) - return string(byt), err -} - -// Coder CLI configuration files. -var ( - Session File = "session" - URL File = "url" -) diff --git a/internal/sync/eventcache.go b/internal/sync/eventcache.go deleted file mode 100644 index 1073b123..00000000 --- a/internal/sync/eventcache.go +++ /dev/null @@ -1,61 +0,0 @@ -package sync - -import ( - "os" - "time" - - "github.com/rjeczalik/notify" -) - -type timedEvent struct { - CreatedAt time.Time - notify.EventInfo -} - -type eventCache map[string]timedEvent - -func (cache eventCache) Add(ev timedEvent) { - lastEvent, ok := cache[ev.Path()] - if ok { - // If the file was quickly created and then destroyed, pretend nothing ever happened. - if lastEvent.Event() == notify.Create && ev.Event() == notify.Remove { - delete(cache, ev.Path()) - return - } - } - // Only let the latest event for a path have action. - cache[ev.Path()] = ev -} - -// SequentialEvents returns the list of events that pertain to directories. -// The set of returned events is disjoint with ConcurrentEvents. -func (cache eventCache) SequentialEvents() []timedEvent { - var r []timedEvent - for _, ev := range cache { - info, err := os.Stat(ev.Path()) - if err == nil && !info.IsDir() { - continue - } - // Include files that have deleted here. - // It's unclear whether they're files or folders. - r = append(r, ev) - } - return r -} - -// ConcurrentEvents returns the list of events that are safe to process after SequentialEvents. -// The set of returns events is disjoint with SequentialEvents. -func (cache eventCache) ConcurrentEvents() []timedEvent { - var r []timedEvent - for _, ev := range cache { - info, err := os.Stat(ev.Path()) - if err != nil { - continue - } - if info.IsDir() { - continue - } - r = append(r, ev) - } - return r -} diff --git a/internal/sync/singlefile.go b/internal/sync/singlefile.go deleted file mode 100644 index 72d7a2a0..00000000 --- a/internal/sync/singlefile.go +++ /dev/null @@ -1,61 +0,0 @@ -package sync - -import ( - "context" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - - "cdr.dev/wsep" - "golang.org/x/xerrors" - "nhooyr.io/websocket" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/coderutil" -) - -// SingleFile copies the given file into the remote dir or remote path of the given coder.Workspace. -func SingleFile(ctx context.Context, local, remoteDir string, workspace *coder.Workspace, client coder.Client) error { - conn, err := coderutil.DialWorkspaceWsep(ctx, client, workspace) - if err != nil { - return xerrors.Errorf("dial remote execer: %w", err) - } - defer func() { _ = conn.Close(websocket.StatusNormalClosure, "normal closure") }() - - if strings.HasSuffix(remoteDir, string(filepath.Separator)) { - remoteDir += filepath.Base(local) - } - - execer := wsep.RemoteExecer(conn) - cmd := fmt.Sprintf(`[ -d %s ] && cat > %s/%s || cat > %s`, remoteDir, remoteDir, filepath.Base(local), remoteDir) - process, err := execer.Start(ctx, wsep.Command{ - Command: "sh", - Args: []string{"-c", cmd}, - Stdin: true, - }) - if err != nil { - return xerrors.Errorf("start sync command: %w", err) - } - defer process.Close() - - sourceFile, err := os.Open(local) - if err != nil { - return xerrors.Errorf("open source file: %w", err) - } - - go func() { _, _ = io.Copy(ioutil.Discard, process.Stdout()) }() - go func() { _, _ = io.Copy(ioutil.Discard, process.Stderr()) }() - go func() { - stdin := process.Stdin() - defer stdin.Close() - _, _ = io.Copy(stdin, sourceFile) - }() - - if err := process.Wait(); err != nil { - return xerrors.Errorf("copy process: %w", err) - } - return nil -} diff --git a/internal/sync/sync.go b/internal/sync/sync.go deleted file mode 100644 index bceead68..00000000 --- a/internal/sync/sync.go +++ /dev/null @@ -1,397 +0,0 @@ -// Package sync contains logic for establishing a file sync between a local machine and a Coder workspace. -package sync - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "os/exec" - "path" - "path/filepath" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/gorilla/websocket" - "github.com/rjeczalik/notify" - "golang.org/x/sync/semaphore" - "golang.org/x/xerrors" - - "cdr.dev/wsep" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/activity" - "cdr.dev/coder-cli/internal/coderutil" - "cdr.dev/coder-cli/pkg/clog" -) - -// Sync runs a live sync daemon. -type Sync struct { - // Init sets whether the sync will do the initial init and then return fast. - Init bool - // LocalDir is an absolute path. - LocalDir string - // RemoteDir is an absolute path. - RemoteDir string - // DisableMetrics disables activity metric pushing. - DisableMetrics bool - - Workspace coder.Workspace - Client coder.Client - OutW io.Writer - ErrW io.Writer - InputReader io.Reader - IsInteractiveOutput bool -} - -// See https://lxadm.com/Rsync_exit_codes#List_of_standard_rsync_exit_codes. -const ( - rsyncExitCodeIncompat = 2 - rsyncExitCodeDataStream = 12 -) - -func (s Sync) syncPaths(delete bool, local, remote string) error { - self := os.Args[0] - - args := []string{"-zz", - "-a", - "--delete", - "-e", self + " sh", local, s.Workspace.Name + ":" + remote, - } - if delete { - args = append([]string{"--delete"}, args...) - } - if os.Getenv("DEBUG_RSYNC") != "" { - args = append([]string{"--progress"}, args...) - } - - // See https://unix.stackexchange.com/questions/188737/does-compression-option-z-with-rsync-speed-up-backup - // on compression level. - // (AB): compression sped up the initial sync of the enterprise repo by 30%, leading me to believe it's - // good in general for codebases. - cmd := exec.Command("rsync", args...) - cmd.Stdout = s.OutW - cmd.Stderr = ioutil.Discard - cmd.Stdin = s.InputReader - - if err := cmd.Run(); err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - switch { - case exitError.ExitCode() == rsyncExitCodeIncompat: - return xerrors.Errorf("no compatible rsync on remote machine: rsync: %w", err) - case exitError.ExitCode() == rsyncExitCodeDataStream: - return xerrors.Errorf("protocol datastream error or no remote rsync found: %w", err) - } - return xerrors.Errorf("rsync: %w", err) - } - return xerrors.Errorf("rsync: %w", err) - } - return nil -} - -func (s Sync) remoteCmd(ctx context.Context, prog string, args ...string) error { - conn, err := coderutil.DialWorkspaceWsep(ctx, s.Client, &s.Workspace) - if err != nil { - return xerrors.Errorf("dial executor: %w", err) - } - defer func() { _ = conn.Close(websocket.CloseNormalClosure, "") }() // Best effort. - - execer := wsep.RemoteExecer(conn) - process, err := execer.Start(ctx, wsep.Command{ - Command: prog, - Args: args, - }) - if err != nil { - return xerrors.Errorf("exec remote process: %w", err) - } - defer process.Close() - - // NOTE: If the copy routine fail, it will result in `process.Wait` to unblock and report an error. - go func() { _, _ = io.Copy(s.OutW, process.Stdout()) }() // Best effort. - go func() { _, _ = io.Copy(s.ErrW, process.Stderr()) }() // Best effort. - - if err := process.Wait(); err != nil { - if code, ok := err.(wsep.ExitError); ok { - return xerrors.Errorf("%s exit status: %d", prog, code) - } - return xerrors.Errorf("execution failure: %w", err) - } - - return nil -} - -// initSync performs the initial synchronization of the directory. -func (s Sync) initSync() error { - clog.LogInfo(fmt.Sprintf("doing initial sync (%s -> %s)", s.LocalDir, s.RemoteDir)) - - start := time.Now() - // Delete old files on initial sync (e.g git checkout). - // Add the "/." to the local directory so rsync doesn't try to place the directory - // into the remote dir. - if err := s.syncPaths(true, s.LocalDir+"/.", s.RemoteDir); err != nil { - return err - } - clog.LogSuccess( - fmt.Sprintf("finished initial sync (%s)", time.Since(start).Truncate(time.Millisecond)), - ) - return nil -} - -func (s Sync) convertPath(local string) string { - relLocalPath, err := filepath.Rel(s.LocalDir, local) - if err != nil { - panic(err) - } - return filepath.Join(s.RemoteDir, relLocalPath) -} - -func (s Sync) handleCreate(localPath string) error { - target := s.convertPath(localPath) - - if err := s.syncPaths(false, localPath, target); err != nil { - // File was quickly deleted. - if _, e1 := os.Stat(localPath); os.IsNotExist(e1) { // NOTE: Discard any other stat error and just expose the syncPath one. - return nil - } - return err - } - return nil -} - -func (s Sync) handleDelete(localPath string) error { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - return s.remoteCmd(ctx, "rm", "-rf", s.convertPath(localPath)) -} - -func (s Sync) handleRename(localPath string) error { - // The rename operation is sent in two events, one - // for the old (gone) file and one for the new file. - // Catching both would require complex state. - // Instead, we turn it into a Create or Delete based - // on file existence. - info, err := os.Stat(localPath) - if err != nil { - if os.IsNotExist(err) { - return s.handleDelete(localPath) - } - return err - } - if info.IsDir() { - // Without this, the directory will be created as a subdirectory. - localPath += "/." - } - return s.handleCreate(localPath) -} - -func (s Sync) work(ev timedEvent) { - var ( - localPath = ev.Path() - err error - ) - switch ev.Event() { - case notify.Write, notify.Create: - err = s.handleCreate(localPath) - case notify.Rename: - err = s.handleRename(localPath) - case notify.Remove: - err = s.handleDelete(localPath) - default: - clog.LogInfo(fmt.Sprintf("unhandled event %+v %s", ev.Event(), ev.Path())) - } - - log := fmt.Sprintf("%v %s (%s)", - ev.Event(), filepath.Base(localPath), time.Since(ev.CreatedAt).Truncate(time.Millisecond*10), - ) - if err != nil { - clog.Log(clog.Error(fmt.Sprintf("%s: %s", log, err))) - } else { - clog.LogSuccess(log) - } -} - -// ErrRestartSync describes a known error case that can be solved by re-starting the command. -var ErrRestartSync = errors.New("the sync exited because it was overloaded, restart it") - -// workEventGroup converges a group of events to prevent duplicate work. -func (s Sync) workEventGroup(evs []timedEvent) { - cache := eventCache{} - for _, ev := range evs { - cache.Add(ev) - } - - // We want to process events concurrently but safely for speed. - // Because the event cache prevents duplicate events for the same file, race conditions of that type - // are impossible. - // What is possible is a dependency on a previous Rename or Create. For example, if a directory is renamed - // and then a file is moved to it. AFAIK this dependecy only exists with Directories. - // So, we sequentially process the list of directory Renames and Creates, and then concurrently - // perform all Writes. - for _, ev := range cache.SequentialEvents() { - s.work(ev) - } - - sem := semaphore.NewWeighted(8) - - var wg sync.WaitGroup - for _, ev := range cache.ConcurrentEvents() { - setConsoleTitle(fmtUpdateTitle(ev.Path()), s.IsInteractiveOutput) - - wg.Add(1) - // TODO: Document why this error is discarded. See https://github.com/cdr/coder-cli/issues/122 for reference. - _ = sem.Acquire(context.Background(), 1) - - ev := ev // Copy the event in the scope to make sure the go routine use the proper value. - go func() { - defer sem.Release(1) - defer wg.Done() - s.work(ev) - }() - } - - wg.Wait() -} - -const ( - // maxinflightInotify sets the maximum number of inotifies before the - // sync just restarts. Syncing a large amount of small files (e.g .git - // or node_modules) is impossible to do performantly with individual - // rsyncs. - maxInflightInotify = 8 - maxEventDelay = 7 * time.Second - // maxAcceptableDispatch is the maximum amount of time before an event - // should begin its journey to the server. This sets a lower bound for - // perceivable latency, but the higher it is, the better the - // optimization. - maxAcceptableDispatch = 50 * time.Millisecond -) - -// Version returns remote protocol version as a string. -// Or, an error if one exists. -func (s Sync) Version() (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - conn, err := coderutil.DialWorkspaceWsep(ctx, s.Client, &s.Workspace) - if err != nil { - return "", xerrors.Errorf("dial workspace executor: %w", err) - } - defer func() { _ = conn.Close(websocket.CloseNormalClosure, "") }() // Best effort. - - execer := wsep.RemoteExecer(conn) - process, err := execer.Start(ctx, wsep.Command{ - Command: "rsync", - Args: []string{"--version"}, - }) - if err != nil { - return "", err - } - defer process.Close() - - buf := &bytes.Buffer{} - _, _ = io.Copy(buf, process.Stdout()) // Ignore error, if any, it would be handled by the process.Wait return. - - if err := process.Wait(); err != nil { - return "", err - } - - firstLine, err := buf.ReadString('\n') - if err != nil { - return "", err - } - - versionString := strings.Split(firstLine, "protocol version ") - - return versionString[1], nil -} - -// Run starts the sync synchronously. -// Use this command to debug what wasn't sync'd correctly: -// rsync -e "coder sh" -nicr ~/Projects/cdr/coder-cli/. ammar:/home/coder/coder-cli/. -func (s Sync) Run() error { - events := make(chan notify.EventInfo, maxInflightInotify) - // Set up a recursive watch. - // We do this before the initial sync so we can capture any changes - // that may have happened during sync. - if err := notify.Watch(path.Join(s.LocalDir, "..."), events, notify.All); err != nil { - return xerrors.Errorf("create watch: %w", err) - } - defer notify.Stop(events) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - if err := s.remoteCmd(ctx, "mkdir", "-p", s.RemoteDir); err != nil { - return xerrors.Errorf("create remote directory: %w", err) - } - - ap := activity.NewPusher(s.Client, s.Workspace.ID, activityName) - ap.Push(ctx) - - setConsoleTitle("⏳ syncing project", s.IsInteractiveOutput) - if err := s.initSync(); err != nil { - return err - } - - if s.Init { - return nil - } - - clog.LogInfo(fmt.Sprintf("watching %s for changes", s.LocalDir)) - - var droppedEvents uint64 - // Timed events lets us track how long each individual file takes to - // update. - timedEvents := make(chan timedEvent, cap(events)) - go func() { - defer close(timedEvents) - for event := range events { - select { - case timedEvents <- timedEvent{ - CreatedAt: time.Now(), - EventInfo: event, - }: - default: - if atomic.AddUint64(&droppedEvents, 1) == 1 { - clog.LogInfo("dropped event, sync should restart soon") - } - } - } - }() - - var eventGroup []timedEvent - - dispatchEventGroup := time.NewTicker(maxAcceptableDispatch) - defer dispatchEventGroup.Stop() - for { - const watchingFilesystemTitle = "🛰 watching filesystem" - setConsoleTitle(watchingFilesystemTitle, s.IsInteractiveOutput) - - select { - case ev := <-timedEvents: - if atomic.LoadUint64(&droppedEvents) > 0 { - return ErrRestartSync - } - eventGroup = append(eventGroup, ev) - case <-dispatchEventGroup.C: - if len(eventGroup) == 0 { - continue - } - // We're too backlogged and should restart the sync. - if time.Since(eventGroup[0].CreatedAt) > maxEventDelay { - return ErrRestartSync - } - s.workEventGroup(eventGroup) - eventGroup = eventGroup[:0] - ap.Push(context.TODO()) - } - } -} - -const activityName = "sync" diff --git a/internal/sync/title.go b/internal/sync/title.go deleted file mode 100644 index ae7630d8..00000000 --- a/internal/sync/title.go +++ /dev/null @@ -1,17 +0,0 @@ -package sync - -import ( - "fmt" - "path/filepath" -) - -func setConsoleTitle(title string, isInteractiveOutput bool) { - if !isInteractiveOutput { - return - } - fmt.Printf("\033]0;%s\007", title) -} - -func fmtUpdateTitle(path string) string { - return "🚀 updating " + filepath.Base(path) -} diff --git a/internal/version/version.go b/internal/version/version.go deleted file mode 100644 index 8873f158..00000000 --- a/internal/version/version.go +++ /dev/null @@ -1,20 +0,0 @@ -// Package version contains the compile-time injected version string and -// related utility methods. -package version - -import ( - "strings" -) - -// Version is populated at compile-time with the current coder-cli version. -var Version string = "unknown" - -// VersionsMatch compares the given APIVersion to the compile-time injected coder-cli version. -func VersionsMatch(apiVersion string) bool { - withoutPatchRelease := strings.Split(Version, ".") - if len(withoutPatchRelease) < 3 { - return false - } - majorMinor := strings.Join(withoutPatchRelease[:2], ".") - return strings.HasPrefix(strings.TrimPrefix(apiVersion, "v"), strings.TrimPrefix(majorMinor, "v")) -} diff --git a/internal/version/version_test.go b/internal/version/version_test.go deleted file mode 100644 index e51b64dd..00000000 --- a/internal/version/version_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package version - -import ( - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" -) - -func TestVersion(t *testing.T) { - Version = "1.12.1" - match := VersionsMatch("1.12.2") - assert.True(t, "versions match", match) - - Version = "v1.14.1" - match = VersionsMatch("1.15.2") - assert.True(t, "versions do not match", !match) - - Version = "v1.15.4" - match = VersionsMatch("1.15.2") - assert.True(t, "versions do match", match) - - Version = "1.15.4" - match = VersionsMatch("v1.15.2") - assert.True(t, "versions do match", match) - - Version = "1.15.4" - match = VersionsMatch("v2.15.2") - assert.True(t, "versions do not match", !match) - - Version = "1.12.2+cli.rc1" - match = VersionsMatch("v1.12.9") - assert.True(t, "versions do match", match) -} diff --git a/internal/x/xcobra/cobra.go b/internal/x/xcobra/cobra.go deleted file mode 100644 index 7ddfd9e2..00000000 --- a/internal/x/xcobra/cobra.go +++ /dev/null @@ -1,25 +0,0 @@ -// Package xcobra wraps the cobra package to provide richer functionality. -package xcobra - -import ( - "fmt" - - "github.com/spf13/cobra" - - "cdr.dev/coder-cli/pkg/clog" -) - -// ExactArgs returns an error if there are not exactly n args. -func ExactArgs(n int) cobra.PositionalArgs { - return func(cmd *cobra.Command, args []string) error { - if len(args) != n { - return clog.Error( - fmt.Sprintf("accepts %d arg(s), received %d", n, len(args)), - clog.Bold("usage: ")+cmd.UseLine(), - clog.BlankLine, - clog.Tipf("use \"--help\" for more info"), - ) - } - return nil - } -} diff --git a/internal/x/xsync/doc.go b/internal/x/xsync/doc.go deleted file mode 100644 index fb23bcce..00000000 --- a/internal/x/xsync/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package xsync provides utilities for concurrency. -package xsync diff --git a/internal/x/xsync/syncwriter.go b/internal/x/xsync/syncwriter.go deleted file mode 100644 index 3a26f262..00000000 --- a/internal/x/xsync/syncwriter.go +++ /dev/null @@ -1,24 +0,0 @@ -package xsync - -import ( - "io" - "sync" -) - -// Writer synchronizes concurrent writes to an underlying writer. -func Writer(w io.Writer) io.Writer { - return &writer{ - w: w, - } -} - -type writer struct { - mu sync.Mutex - w io.Writer -} - -func (sw *writer) Write(b []byte) (int, error) { - sw.mu.Lock() - defer sw.mu.Unlock() - return sw.w.Write(b) -} diff --git a/internal/x/xterminal/doc.go b/internal/x/xterminal/doc.go deleted file mode 100644 index 21e0ae0e..00000000 --- a/internal/x/xterminal/doc.go +++ /dev/null @@ -1,13 +0,0 @@ -// Package xterminal provides functions to change termios or console attributes -// and restore them later on. It supports Unix and Windows. -// -// This does the same thing as x/crypto/ssh/terminal on Linux. On Windows, it -// sets the same console modes as the terminal package but also sets -// `ENABLE_VIRTUAL_TERMINAL_INPUT` and `ENABLE_VIRTUAL_TERMINAL_PROCESSING` to -// allow for VT100 sequences in the console. This is important, otherwise Linux -// apps (with colors or ncurses) that are run through SSH or wsep get -// garbled in a Windows console. -// -// More details can be found out about Windows console modes here: -// https://docs.microsoft.com/en-us/windows/console/setconsolemode -package xterminal diff --git a/internal/x/xterminal/terminal.go b/internal/x/xterminal/terminal.go deleted file mode 100644 index a431fd1a..00000000 --- a/internal/x/xterminal/terminal.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build !windows -// +build !windows - -package xterminal - -import ( - "golang.org/x/term" -) - -// State differs per-platform. -type State struct { - s *term.State -} - -// MakeOutputRaw does nothing on non-Windows platforms. -func MakeOutputRaw(fd uintptr) (*State, error) { return nil, nil } - -// Restore terminal back to original state. -func Restore(fd uintptr, state *State) error { - if state == nil { - return nil - } - - return term.Restore(int(fd), state.s) -} diff --git a/internal/x/xterminal/terminal_windows.go b/internal/x/xterminal/terminal_windows.go deleted file mode 100644 index bf16f099..00000000 --- a/internal/x/xterminal/terminal_windows.go +++ /dev/null @@ -1,49 +0,0 @@ -//go:build windows -// +build windows - -package xterminal - -import ( - "golang.org/x/sys/windows" -) - -// State differs per-platform. -type State struct { - mode uint32 -} - -// makeRaw sets the terminal in raw mode and returns the previous state so it can be restored. -func makeRaw(handle windows.Handle, input bool) (uint32, error) { - var prevState uint32 - if err := windows.GetConsoleMode(handle, &prevState); err != nil { - return 0, err - } - - var raw uint32 - if input { - raw = prevState &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) - raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT - } else { - raw = prevState | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING - } - - if err := windows.SetConsoleMode(handle, raw); err != nil { - return 0, err - } - return prevState, nil -} - -// MakeOutputRaw sets an output terminal to raw and enables VT100 processing. -func MakeOutputRaw(handle uintptr) (*State, error) { - prevState, err := makeRaw(windows.Handle(handle), false) - if err != nil { - return nil, err - } - - return &State{mode: prevState}, nil -} - -// Restore terminal back to original state. -func Restore(handle uintptr, state *State) error { - return windows.SetConsoleMode(windows.Handle(handle), state.mode) -} diff --git a/pkg/clog/clog.go b/pkg/clog/clog.go deleted file mode 100644 index ebdce686..00000000 --- a/pkg/clog/clog.go +++ /dev/null @@ -1,135 +0,0 @@ -package clog - -import ( - "errors" - "fmt" - "io" - "os" - "strings" - - "github.com/fatih/color" - "golang.org/x/xerrors" -) - -var writer io.Writer = os.Stderr - -// SetOutput sets the package-level writer target for log functions. -func SetOutput(w io.Writer) { - writer = w -} - -// CLIMessage provides a human-readable message for CLI errors and messages. -type CLIMessage struct { - Level string - Color color.Attribute - Header string - Lines []string -} - -// CLIError wraps a CLIMessage and allows consumers to treat it as a normal error. -type CLIError struct { - CLIMessage - error -} - -// String formats the CLI message for consumption by a human. -func (m CLIMessage) String() string { - var str strings.Builder - fmt.Fprintf(&str, "%s: %s\r\n", - color.New(m.Color).Sprint(m.Level), - color.New(color.Bold).Sprint(m.Header)) - for _, line := range m.Lines { - fmt.Fprintf(&str, " %s %s\r\n", color.New(m.Color).Sprint("|"), line) - } - return str.String() -} - -// Log logs the given error to stderr, defaulting to "fatal" if the error is not a CLIError. -// If the error is a CLIError, the plain error chain is ignored and the CLIError -// is logged on its own. -func Log(err error) { - var cliErr CLIError - if !xerrors.As(err, &cliErr) { - cliErr = Fatal(err.Error()) - } - fmt.Fprintln(writer, cliErr.String()) -} - -// LogInfo prints the given info message to stderr. -func LogInfo(header string, lines ...string) { - fmt.Fprint(writer, CLIMessage{ - Level: "info", - Color: color.FgBlue, - Header: header, - Lines: lines, - }.String()) -} - -// LogSuccess prints the given info message to stderr. -func LogSuccess(header string, lines ...string) { - fmt.Fprint(writer, CLIMessage{ - Level: "success", - Color: color.FgGreen, - Header: header, - Lines: lines, - }.String()) -} - -// LogWarn prints the given warn message to stderr. -func LogWarn(header string, lines ...string) { - fmt.Fprint(writer, CLIMessage{ - Level: "warning", - Color: color.FgYellow, - Header: header, - Lines: lines, - }.String()) -} - -// Error creates an error with the level "error". -func Error(header string, lines ...string) CLIError { - return CLIError{ - CLIMessage: CLIMessage{ - Color: color.FgRed, - Level: "error", - Header: header, - Lines: lines, - }, - error: errors.New(header), - } -} - -// Fatal creates an error with the level "fatal". -func Fatal(header string, lines ...string) CLIError { - return CLIError{ - CLIMessage: CLIMessage{ - Color: color.FgRed, - Level: "fatal", - Header: header, - Lines: lines, - }, - error: errors.New(header), - } -} - -// Bold provides a convenience wrapper around color.New for brevity when logging. -func Bold(a string) string { - return color.New(color.Bold).Sprint(a) -} - -// Tipf formats according to the given format specifier and prepends a bolded "tip: " header. -func Tipf(format string, a ...interface{}) string { - return fmt.Sprintf("%s %s", Bold("tip:"), fmt.Sprintf(format, a...)) -} - -// Hintf formats according to the given format specifier and prepends a bolded "hint: " header. -func Hintf(format string, a ...interface{}) string { - return fmt.Sprintf("%s %s", Bold("hint:"), fmt.Sprintf(format, a...)) -} - -// Causef formats according to the given format specifier and prepends a bolded "cause: " header. -func Causef(format string, a ...interface{}) string { - return fmt.Sprintf("%s %s", Bold("cause:"), fmt.Sprintf(format, a...)) -} - -// BlankLine is an empty string meant to be used in CLIMessage and CLIError construction. -const BlankLine = "" diff --git a/pkg/clog/clog_test.go b/pkg/clog/clog_test.go deleted file mode 100644 index 6967eb3c..00000000 --- a/pkg/clog/clog_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package clog - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" - "golang.org/x/xerrors" -) - -func TestError(t *testing.T) { - t.Run("oneline", func(t *testing.T) { - var mockErr error = Error("fake error") - mockErr = xerrors.Errorf("wrap 1: %w", mockErr) - mockErr = fmt.Errorf("wrap 2: %w", mockErr) - - var buf bytes.Buffer - //! clearly not concurrent safe - SetOutput(&buf) - - Log(mockErr) - - output, err := ioutil.ReadAll(&buf) - assert.Success(t, "read all stderr output", err) - - assert.Equal(t, "output is as expected", "error: fake error\r\n\n", string(output)) - }) - - t.Run("plain-error", func(t *testing.T) { - mockErr := xerrors.Errorf("base error") - mockErr = fmt.Errorf("wrap 1: %w", mockErr) - - var buf bytes.Buffer - //! clearly not concurrent safe - SetOutput(&buf) - - Log(mockErr) - - output, err := ioutil.ReadAll(&buf) - assert.Success(t, "read all stderr output", err) - - assert.Equal(t, "output is as expected", "fatal: wrap 1: base error\r\n\n", string(output)) - }) - - t.Run("message", func(t *testing.T) { - for _, f := range []struct { - f func(string, ...string) - level string - }{{LogInfo, "info"}, {LogSuccess, "success"}, {LogWarn, "warning"}} { - var buf bytes.Buffer - //! clearly not concurrent safe - SetOutput(&buf) - - f.f("testing", Hintf("maybe do %q", "this"), BlankLine, Causef("what happened was %q", "this")) - - output, err := ioutil.ReadAll(&buf) - assert.Success(t, "read all stderr output", err) - - assert.Equal(t, "output is as expected", f.level+": testing\r\n | hint: maybe do \"this\"\r\n | \r\n | cause: what happened was \"this\"\r\n", string(output)) - } - }) - - t.Run("multi-line", func(t *testing.T) { - var mockErr error = Error("fake header", "next line", BlankLine, Tipf("content of fake tip")) - mockErr = xerrors.Errorf("wrap 1: %w", mockErr) - mockErr = fmt.Errorf("wrap 1: %w", mockErr) - - var buf bytes.Buffer - //! clearly not concurrent safe - SetOutput(&buf) - - Log(mockErr) - - output, err := ioutil.ReadAll(&buf) - assert.Success(t, "read all stderr output", err) - - assert.Equal(t, - "output is as expected", - "error: fake header\r\n | next line\r\n | \r\n | tip: content of fake tip\r\n\n", - string(output), - ) - }) -} diff --git a/pkg/clog/doc.go b/pkg/clog/doc.go deleted file mode 100644 index 9e5717bb..00000000 --- a/pkg/clog/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package clog provides rich error types and logging helpers for coder-cli. -// -// clog encourages returning error types rather than -// logging them and failing with os.Exit as they happen. -// Error, Fatal, and Warn allow downstream functions to return errors with rich formatting information -// while preserving the original, single-line error chain. -package clog diff --git a/pkg/clog/errgroup.go b/pkg/clog/errgroup.go deleted file mode 100644 index a96d6449..00000000 --- a/pkg/clog/errgroup.go +++ /dev/null @@ -1,58 +0,0 @@ -package clog - -import ( - "fmt" - "sync/atomic" - - "golang.org/x/sync/errgroup" - "golang.org/x/xerrors" -) - -// ErrGroup wraps the /x/sync/errgroup.(Group) and adds clog logging and rich error propagation. -// -// Take for example, a case in which we are concurrently stopping a slice of workspaces. -// In this case, we want to log errors as they happen, not pass them through the callstack as errors. -// When the operations complete, we want to log how many, if any, failed. The caller is still expected -// to handle success and info logging. -type ErrGroup interface { - Go(f func() error) - Wait() error -} - -type group struct { - egroup errgroup.Group - failures int32 -} - -// LoggedErrGroup gives an error group with error logging and error propagation handled automatically. -func LoggedErrGroup() ErrGroup { - return &group{ - egroup: errgroup.Group{}, - failures: 0, - } -} - -func (g *group) Go(f func() error) { - g.egroup.Go(func() error { - if err := f(); err != nil { - atomic.AddInt32(&g.failures, 1) - Log(err) - - // this error does not matter because we discard it in Wait. - return xerrors.New("") - } - return nil - }) -} - -func (g *group) Wait() error { - _ = g.egroup.Wait() // ignore this error because we are already tracking failures manually - if g.failures == 0 { - return nil - } - failureWord := "failure" - if g.failures > 1 { - failureWord += "s" - } - return Fatal(fmt.Sprintf("%d %s emitted", g.failures, failureWord)) -} diff --git a/pkg/clog/errgroup_test.go b/pkg/clog/errgroup_test.go deleted file mode 100644 index 0d209d81..00000000 --- a/pkg/clog/errgroup_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package clog - -import ( - "bytes" - "errors" - "strings" - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/internal/x/xsync" -) - -func TestErrGroup(t *testing.T) { - t.Run("success", func(t *testing.T) { - egroup := LoggedErrGroup() - - var buf bytes.Buffer - SetOutput(xsync.Writer(&buf)) - - egroup.Go(func() error { return nil }) - egroup.Go(func() error { return nil }) - egroup.Go(func() error { return nil }) - - err := egroup.Wait() - assert.Success(t, "error group wait", err) - assert.Equal(t, "empty log buffer", "", buf.String()) - }) - t.Run("failure_count", func(t *testing.T) { - egroup := LoggedErrGroup() - - var buf bytes.Buffer - SetOutput(xsync.Writer(&buf)) - - egroup.Go(func() error { return errors.New("whoops") }) - egroup.Go(func() error { return Error("rich error", "second line") }) - - err := egroup.Wait() - assert.ErrorContains(t, "error group wait", err, "2 failures emitted") - assert.True(t, "log buf contains", strings.Contains(buf.String(), "fatal: whoops\r\n\n")) - assert.True(t, "log buf contains", strings.Contains(buf.String(), "error: rich error\r\n | second line\r\n\n")) - }) -} diff --git a/pkg/proto/doc.go b/pkg/proto/doc.go deleted file mode 100644 index 65f4d1e5..00000000 --- a/pkg/proto/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package proto contains shared messages for webrtc handshakes. -package proto diff --git a/pkg/tablewriter/doc.go b/pkg/tablewriter/doc.go deleted file mode 100644 index 366a7b9e..00000000 --- a/pkg/tablewriter/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package tablewriter provides helpers for printing human-readable tabular data from slices of structs. -package tablewriter diff --git a/pkg/tablewriter/table_output.golden b/pkg/tablewriter/table_output.golden deleted file mode 100755 index dfe3299c..00000000 --- a/pkg/tablewriter/table_output.golden +++ /dev/null @@ -1,3 +0,0 @@ -Name birthday month first_nested second_nested Age -Tom 12 234-0934 2340-234234 28.12 -Jerry 3 aflfafe-afjlk falj-fjlkjlkadf 36.22 diff --git a/pkg/tablewriter/tablewriter.go b/pkg/tablewriter/tablewriter.go deleted file mode 100644 index 9f12becd..00000000 --- a/pkg/tablewriter/tablewriter.go +++ /dev/null @@ -1,96 +0,0 @@ -package tablewriter - -import ( - "fmt" - "io" - "reflect" - "strings" - "text/tabwriter" -) - -const structFieldTagKey = "table" - -// StructValues tab delimits the values of a given struct. -// -// Tag a field `table:"-"` to hide it from output. -// Tag a field `table:"_"` to flatten its subfields. -func StructValues(data interface{}) string { - v := reflect.ValueOf(data) - s := &strings.Builder{} - for i := 0; i < v.NumField(); i++ { - if shouldHideField(v.Type().Field(i)) { - continue - } - if shouldFlatten(v.Type().Field(i)) { - fmt.Fprintf(s, "%v", StructValues(v.Field(i).Interface())) - continue - } - fmt.Fprintf(s, "%v\t", v.Field(i).Interface()) - } - return s.String() -} - -// StructFieldNames tab delimits the field names of a given struct. -// -// Tag a field `table:"-"` to hide it from output. -// Tag a field `table:"_"` to flatten its subfields. -func StructFieldNames(data interface{}) string { - v := reflect.ValueOf(data) - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - s := &strings.Builder{} - for i := 0; i < v.NumField(); i++ { - field := v.Type().Field(i) - if shouldHideField(field) { - continue - } - if shouldFlatten(field) { - fmt.Fprintf(s, "%s", StructFieldNames(reflect.New(field.Type).Interface())) - continue - } - fmt.Fprintf(s, "%s\t", fieldName(field)) - } - return s.String() -} - -// WriteTable writes the given list elements to stdout in a human readable -// tabular format. Headers abide by the `table` struct tag. -// -// `table:"-"` omits the field and no tag defaults to the Go identifier. -// `table:"_"` flattens a fields subfields. -func WriteTable(writer io.Writer, length int, each func(i int) interface{}) error { - if length < 1 { - return nil - } - w := tabwriter.NewWriter(writer, 0, 0, 4, ' ', 0) - defer func() { _ = w.Flush() }() // Best effort. - for ix := 0; ix < length; ix++ { - item := each(ix) - if ix == 0 { - if _, err := fmt.Fprintln(w, StructFieldNames(item)); err != nil { - return err - } - } - if _, err := fmt.Fprintln(w, StructValues(item)); err != nil { - return err - } - } - return nil -} - -func fieldName(f reflect.StructField) string { - custom, ok := f.Tag.Lookup(structFieldTagKey) - if ok { - return custom - } - return f.Name -} - -func shouldFlatten(f reflect.StructField) bool { - return f.Tag.Get(structFieldTagKey) == "_" -} - -func shouldHideField(f reflect.StructField) bool { - return f.Tag.Get(structFieldTagKey) == "-" -} diff --git a/pkg/tablewriter/tablewriter_test.go b/pkg/tablewriter/tablewriter_test.go deleted file mode 100644 index e611e52c..00000000 --- a/pkg/tablewriter/tablewriter_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package tablewriter - -import ( - "bytes" - "flag" - "io/ioutil" - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" -) - -var write = flag.Bool("write", false, "write to the golden files") - -func TestTableWriter(t *testing.T) { - type NestedRow struct { - NestedOne string `table:"first_nested"` - NestedTwo string `table:"second_nested"` - } - - type Row struct { - ID string `table:"-"` - Name string - BirthdayMonth int `table:"birthday month"` - Nested NestedRow `table:"_"` - Age float32 - } - - items := []Row{ - { - ID: "13123lkjqlkj-2f323l--f23f", - Name: "Tom", - BirthdayMonth: 12, - Age: 28.12, - Nested: NestedRow{ - NestedOne: "234-0934", - NestedTwo: "2340-234234", - }, - }, - { - ID: "afwaflkj23kl-2f323l--f23f", - Name: "Jerry", - BirthdayMonth: 3, - Age: 36.22, - Nested: NestedRow{ - NestedOne: "aflfafe-afjlk", - NestedTwo: "falj-fjlkjlkadf", - }, - }, - } - - buf := bytes.NewBuffer(nil) - err := WriteTable(buf, len(items), func(i int) interface{} { return items[i] }) - assert.Success(t, "write table", err) - - assertGolden(t, "table_output.golden", buf.Bytes()) -} - -func assertGolden(t *testing.T, path string, output []byte) { - if *write { - err := ioutil.WriteFile(path, output, 0777) - assert.Success(t, "write file", err) - return - } - goldenContent, err := ioutil.ReadFile(path) - assert.Success(t, "read golden file", err) - assert.Equal(t, "golden content matches", string(goldenContent), string(output)) -} diff --git a/pkg/tcli/doc.go b/pkg/tcli/doc.go deleted file mode 100644 index 561dc480..00000000 --- a/pkg/tcli/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package tcli provides a framework for CLI integration testing. -// Execute commands on the raw host or inside a docker container. -// Define custom Assertion types to extend test functionality. -package tcli diff --git a/pkg/tcli/tcli.go b/pkg/tcli/tcli.go deleted file mode 100644 index b09f4885..00000000 --- a/pkg/tcli/tcli.go +++ /dev/null @@ -1,352 +0,0 @@ -package tcli - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "os/exec" - "regexp" - "strings" - "testing" - "time" - - "cdr.dev/slog" - "cdr.dev/slog/sloggers/slogtest" - "cdr.dev/slog/sloggers/slogtest/assert" - "golang.org/x/xerrors" -) - -var ( - _ runnable = &ContainerRunner{} - _ runnable = &HostRunner{} -) - -type runnable interface { - Run(ctx context.Context, command string) *Assertable - RunCmd(cmd *exec.Cmd) *Assertable - io.Closer -} - -// ContainerConfig describes the ContainerRunner configuration schema for initializing a testing environment. -type ContainerConfig struct { - Name string - Image string - BindMounts map[string]string -} - -func mountArgs(m map[string]string) (args []string) { - for src, dest := range m { - args = append(args, "--mount", fmt.Sprintf("type=bind,source=%s,target=%s", src, dest)) - } - return args -} - -func preflightChecks() error { - _, err := exec.LookPath("docker") - if err != nil { - return xerrors.Errorf(`"docker" not found in $PATH`) - } - return nil -} - -// ContainerRunner specifies a runtime container for performing command tests. -type ContainerRunner struct { - name string - ctx context.Context -} - -// NewContainerRunner starts a new docker container for executing command tests. -func NewContainerRunner(ctx context.Context, config *ContainerConfig) (*ContainerRunner, error) { - if err := preflightChecks(); err != nil { - return nil, err - } - - args := []string{ - "run", - "--name", config.Name, - "--network", "host", - "--rm", "-it", "-d", - } - args = append(args, mountArgs(config.BindMounts)...) - args = append(args, config.Image) - - cmd := exec.CommandContext(ctx, "docker", args...) - - out, err := cmd.CombinedOutput() - if err != nil { - return nil, xerrors.Errorf( - "start testing container %q, (%s): %w", - config.Name, string(out), err) - } - - return &ContainerRunner{ - name: config.Name, - ctx: ctx, - }, nil -} - -// Close kills and removes the command execution testing container. -func (r *ContainerRunner) Close() error { - cmd := exec.CommandContext(r.ctx, - "sh", "-c", strings.Join([]string{ - "docker", "kill", r.name, "&&", - "docker", "rm", r.name, - }, " ")) - - out, err := cmd.CombinedOutput() - if err != nil { - return xerrors.Errorf( - "stop testing container %q, (%s): %w", - r.name, string(out), err) - } - return nil -} - -// Run executes the given command in the runtime container with reasonable defaults. -// "command" is executed in a shell as an argument to "sh -c". -func (r *ContainerRunner) Run(ctx context.Context, command string) *Assertable { - cmd := exec.CommandContext(ctx, - "docker", "exec", "-i", r.name, - "sh", "-c", command, - ) - - return &Assertable{ - cmd: cmd, - tname: command, - } -} - -// RunCmd lifts the given *exec.Cmd into the runtime container. -func (r *ContainerRunner) RunCmd(cmd *exec.Cmd) *Assertable { - path, _ := exec.LookPath("docker") - cmd.Path = path - command := strings.Join(cmd.Args, " ") - cmd.Args = append([]string{"docker", "exec", "-i", r.name}, cmd.Args...) - - return &Assertable{ - cmd: cmd, - tname: command, - } -} - -// HostRunner executes command tests on the host, outside of a container. -type HostRunner struct{} - -// Run executes the given command on the host. -// "command" is executed in a shell as an argument to "sh -c". -func (r *HostRunner) Run(ctx context.Context, command string) *Assertable { - cmd := exec.CommandContext(ctx, "sh", "-c", command) - - return &Assertable{ - cmd: cmd, - tname: command, - } -} - -// RunCmd executes the given *exec.Cmd on the host. -func (r *HostRunner) RunCmd(cmd *exec.Cmd) *Assertable { - return &Assertable{ - cmd: cmd, - tname: strings.Join(cmd.Args, " "), - } -} - -// Close is a noop for HostRunner. -func (r *HostRunner) Close() error { - return nil -} - -// Assertable describes an initialized command ready to be run and asserted against. -type Assertable struct { - cmd *exec.Cmd - tname string -} - -// Assert runs the Assertable and. -func (a *Assertable) Assert(t *testing.T, option ...Assertion) { - slog.Helper() - var ( - stdout bytes.Buffer - stderr bytes.Buffer - result CommandResult - ) - if a.cmd == nil { - slogtest.Fatal(t, "test failed to initialize: no command specified") - } - - a.cmd.Stdout = &stdout - a.cmd.Stderr = &stderr - - start := time.Now() - err := a.cmd.Run() - result.Duration = time.Since(start) - - if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - result.ExitCode = exitErr.ExitCode() - } else { - slogtest.Fatal(t, "command failed to run", slog.Error(err), slog.F("command", a.cmd)) - } - } else { - result.ExitCode = 0 - } - - result.Stdout = stdout.Bytes() - result.Stderr = stderr.Bytes() - - slogtest.Info(t, "command output", - slog.F("command", a.cmd), - slog.F("stdout", string(result.Stdout)), - slog.F("stderr", string(result.Stderr)), - slog.F("exit_code", result.ExitCode), - slog.F("duration", result.Duration), - ) - - for _, assertion := range option { - assertion(t, &result) - } -} - -// Assertion specifies an assertion on the given CommandResult. -// Pass custom Assertion functions to cover special cases. -type Assertion func(t *testing.T, r *CommandResult) - -// CommandResult contains the aggregated result of a command execution. -type CommandResult struct { - Stdout, Stderr []byte - ExitCode int - Duration time.Duration -} - -// Success asserts that the command exited with an exit code of 0. -func Success() Assertion { - slog.Helper() - return ExitCodeIs(0) -} - -// Error asserts that the command exited with a nonzero exit code. -func Error() Assertion { - return func(t *testing.T, r *CommandResult) { - slog.Helper() - assert.True(t, "exit code is nonzero", r.ExitCode != 0) - } -} - -// ExitCodeIs asserts that the command exited with the given code. -func ExitCodeIs(code int) Assertion { - return func(t *testing.T, r *CommandResult) { - slog.Helper() - assert.Equal(t, "exit code is as expected", code, r.ExitCode) - } -} - -// StdoutEmpty asserts that the command did not write any data to Stdout. -func StdoutEmpty() Assertion { - return func(t *testing.T, r *CommandResult) { - slog.Helper() - empty(t, "stdout", r.Stdout) - } -} - -// GetResult offers an escape hatch from tcli -// The pointer passed as "result" will be assigned to the command's *CommandResult. -func GetResult(result **CommandResult) Assertion { - return func(t *testing.T, r *CommandResult) { - slog.Helper() - *result = r - } -} - -// StderrEmpty asserts that the command did not write any data to Stderr. -func StderrEmpty() Assertion { - return func(t *testing.T, r *CommandResult) { - slog.Helper() - empty(t, "stderr", r.Stderr) - } -} - -// StdoutMatches asserts that Stdout contains a substring which matches the given regexp. -func StdoutMatches(pattern string) Assertion { - return func(t *testing.T, r *CommandResult) { - slog.Helper() - matches(t, "stdout", pattern, r.Stdout) - } -} - -// StderrMatches asserts that Stderr contains a substring which matches the given regexp. -func StderrMatches(pattern string) Assertion { - return func(t *testing.T, r *CommandResult) { - slog.Helper() - matches(t, "stderr", pattern, r.Stderr) - } -} - -func matches(t *testing.T, name, pattern string, target []byte) { - slog.Helper() - fields := []slog.Field{ - slog.F("pattern", pattern), - slog.F("target", string(target)), - slog.F("sink", name), - } - - ok, err := regexp.Match(pattern, target) - if err != nil { - slogtest.Fatal(t, "attempt regexp match", append(fields, slog.Error(err))...) - } - if !ok { - slogtest.Fatal(t, "expected to find pattern, no match found", fields...) - } -} - -func empty(t *testing.T, name string, a []byte) { - slog.Helper() - if len(a) > 0 { - slogtest.Fatal(t, "expected "+name+" to be empty", slog.F("got", string(a))) - } -} - -// DurationLessThan asserts that the command completed in less than the given duration. -func DurationLessThan(dur time.Duration) Assertion { - return func(t *testing.T, r *CommandResult) { - slog.Helper() - if r.Duration > dur { - slogtest.Fatal(t, "duration longer than expected", - slog.F("expected_less_than", dur.String), - slog.F("actual", r.Duration.String()), - ) - } - } -} - -// DurationGreaterThan asserts that the command completed in greater than the given duration. -func DurationGreaterThan(dur time.Duration) Assertion { - return func(t *testing.T, r *CommandResult) { - slog.Helper() - if r.Duration < dur { - slogtest.Fatal(t, "duration shorter than expected", - slog.F("expected_greater_than", dur.String), - slog.F("actual", r.Duration.String()), - ) - } - } -} - -// StdoutJSONUnmarshal attempts to unmarshal stdout into the given target. -func StdoutJSONUnmarshal(target interface{}) Assertion { - return func(t *testing.T, r *CommandResult) { - slog.Helper() - err := json.Unmarshal(r.Stdout, target) - assert.Success(t, "stdout json unmarshals", err) - } -} - -// StderrJSONUnmarshal attempts to unmarshal stderr into the given target. -func StderrJSONUnmarshal(target interface{}) Assertion { - return func(t *testing.T, r *CommandResult) { - slog.Helper() - err := json.Unmarshal(r.Stdout, target) - assert.Success(t, "stderr json unmarshals", err) - } -} diff --git a/pkg/tcli/tcli_test.go b/pkg/tcli/tcli_test.go deleted file mode 100644 index 178e702c..00000000 --- a/pkg/tcli/tcli_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package tcli_test - -import ( - "context" - "os" - "os/exec" - "strings" - "testing" - "time" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/pkg/tcli" -) - -func TestTCli(t *testing.T) { - t.Parallel() - ctx := context.Background() - - container, err := tcli.NewContainerRunner(ctx, &tcli.ContainerConfig{ - Image: "ubuntu:latest", - Name: "test-container", - }) - assert.Success(t, "new run container", err) - defer container.Close() - - container.Run(ctx, "echo testing").Assert(t, - tcli.Success(), - tcli.StderrEmpty(), - tcli.StdoutMatches("esting"), - ) - - container.Run(ctx, "sleep 1.5 && echo 1>&2 stderr-message").Assert(t, - tcli.Success(), - tcli.StdoutEmpty(), - tcli.StderrMatches("message"), - tcli.DurationGreaterThan(time.Second), - ) - - cmd := exec.CommandContext(ctx, "cat") - cmd.Stdin = strings.NewReader("testing") - - container.RunCmd(cmd).Assert(t, - tcli.Success(), - tcli.StderrEmpty(), - tcli.StdoutMatches("testing"), - ) -} -func TestHostRunner(t *testing.T) { - t.Parallel() - var ( - c tcli.HostRunner - ctx = context.Background() - ) - - c.Run(ctx, "echo testing").Assert(t, - tcli.Success(), - tcli.StderrEmpty(), - tcli.StdoutMatches("testing"), - ) - - wd, err := os.Getwd() - assert.Success(t, "get working dir", err) - - c.Run(ctx, "pwd").Assert(t, - tcli.Success(), - tcli.StderrEmpty(), - tcli.StdoutMatches(wd), - ) -} diff --git a/version b/version deleted file mode 100644 index dd43a143..00000000 --- a/version +++ /dev/null @@ -1 +0,0 @@ -1.26.1 diff --git a/wsnet/cache.go b/wsnet/cache.go deleted file mode 100644 index fa5e561f..00000000 --- a/wsnet/cache.go +++ /dev/null @@ -1,170 +0,0 @@ -package wsnet - -import ( - "context" - "errors" - "sync" - "time" - - "github.com/pion/webrtc/v3" - "golang.org/x/sync/singleflight" -) - -// DialCache constructs a new DialerCache. -// The cache clears connections that: -// 1. Are older than the TTL and have no active user-created connections. -// 2. Have been closed. -func DialCache(ttl time.Duration) *DialerCache { - dc := &DialerCache{ - ttl: ttl, - closed: make(chan struct{}), - flightGroup: &singleflight.Group{}, - mut: sync.RWMutex{}, - dialers: make(map[string]*Dialer), - atime: make(map[string]time.Time), - } - go dc.init() - return dc -} - -type DialerCache struct { - ttl time.Duration - flightGroup *singleflight.Group - closed chan struct{} - mut sync.RWMutex - - // Key is the "key" of a dialer, which is usually the workspace ID. - dialers map[string]*Dialer - atime map[string]time.Time -} - -// init starts the ticker for evicting connections. -func (d *DialerCache) init() { - ticker := time.NewTicker(time.Second * 5) - defer ticker.Stop() - for { - select { - case <-d.closed: - return - case <-ticker.C: - d.evict() - } - } -} - -// evict removes lost/broken/expired connections from the cache. -func (d *DialerCache) evict() { - var wg sync.WaitGroup - d.mut.RLock() - for key, dialer := range d.dialers { - wg.Add(1) - key := key - dialer := dialer - go func() { - defer wg.Done() - - // If we're no longer signaling, the connection is pending close. - evict := dialer.rtc.SignalingState() == webrtc.SignalingStateClosed - - // HACK: since the pion package can't reuse data channel IDs we need - // to terminate the connection once we approach the critical number. - // We're working on adding data channel ID reuse support upstream. - stats, ok := dialer.rtc.GetStats().GetConnectionStats(dialer.rtc) - if ok && stats.DataChannelsRequested > 32500 { - evict = true - } - - if dialer.activeConnections() == 0 && time.Since(d.atime[key]) >= d.ttl { - evict = true - } else { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) - defer cancel() - err := dialer.Ping(ctx) - if err != nil { - evict = true - } - } - - if !evict { - return - } - - _ = dialer.Close() - // Ensure after Ping and potential delays that we're still testing against - // the proper dialer. - if dialer != d.dialers[key] { - return - } - - d.mut.Lock() - defer d.mut.Unlock() - delete(d.atime, key) - delete(d.dialers, key) - }() - } - d.mut.RUnlock() - wg.Wait() -} - -// Dial returns a Dialer from the cache if one exists with the key provided, -// or dials a new connection using the dialerFunc. -// The bool returns whether the connection was found in the cache or not. -func (d *DialerCache) Dial(ctx context.Context, key string, dialerFunc func() (*Dialer, error)) (*Dialer, bool, error) { - select { - case <-d.closed: - return nil, false, errors.New("cache closed") - default: - } - - d.mut.RLock() - dialer, ok := d.dialers[key] - d.mut.RUnlock() - if ok { - d.mut.Lock() - d.atime[key] = time.Now() - d.mut.Unlock() - - // The connection is pending close here... - if dialer.rtc.SignalingState() != webrtc.SignalingStateClosed { - return dialer, true, nil - } - } - - rawDialer, err, _ := d.flightGroup.Do(key, func() (interface{}, error) { - dialer, err := dialerFunc() - if err != nil { - return nil, err - } - d.mut.Lock() - d.dialers[key] = dialer - d.atime[key] = time.Now() - d.mut.Unlock() - - return dialer, nil - }) - if err != nil { - return nil, false, err - } - select { - case <-d.closed: - return nil, false, errors.New("cache closed") - default: - } - - return rawDialer.(*Dialer), false, nil -} - -// Close closes all cached dialers. -func (d *DialerCache) Close() error { - d.mut.Lock() - defer d.mut.Unlock() - - for _, dialer := range d.dialers { - err := dialer.Close() - if err != nil { - return err - } - } - close(d.closed) - return nil -} diff --git a/wsnet/cache_test.go b/wsnet/cache_test.go deleted file mode 100644 index 44edb608..00000000 --- a/wsnet/cache_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package wsnet - -import ( - "context" - "testing" - "time" - - "cdr.dev/slog/sloggers/slogtest" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCache(t *testing.T) { - dialFunc := func(connectAddr string) func() (*Dialer, error) { - return func() (*Dialer, error) { - return DialWebsocket(context.Background(), connectAddr, nil, nil) - } - } - - t.Run("Caches", func(t *testing.T) { - connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") - require.NoError(t, err) - defer l.Close() - - cache := DialCache(time.Hour) - c1, cached, err := cache.Dial(context.Background(), "example", dialFunc(connectAddr)) - require.NoError(t, err) - require.Equal(t, cached, false) - c2, cached, err := cache.Dial(context.Background(), "example", dialFunc(connectAddr)) - require.NoError(t, err) - require.Equal(t, cached, true) - assert.Same(t, c1, c2) - }) - - t.Run("Create If Closed", func(t *testing.T) { - connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") - require.NoError(t, err) - defer l.Close() - - cache := DialCache(time.Hour) - - c1, cached, err := cache.Dial(context.Background(), "example", dialFunc(connectAddr)) - require.NoError(t, err) - require.Equal(t, cached, false) - require.NoError(t, c1.Close()) - c2, cached, err := cache.Dial(context.Background(), "example", dialFunc(connectAddr)) - require.NoError(t, err) - require.Equal(t, cached, false) - assert.NotSame(t, c1, c2) - }) - - t.Run("Evict No Connections", func(t *testing.T) { - connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") - require.NoError(t, err) - defer l.Close() - - cache := DialCache(0) - - c1, cached, err := cache.Dial(context.Background(), "example", dialFunc(connectAddr)) - require.NoError(t, err) - require.Equal(t, cached, false) - cache.evict() - c2, cached, err := cache.Dial(context.Background(), "example", dialFunc(connectAddr)) - require.NoError(t, err) - require.Equal(t, cached, false) - assert.NotSame(t, c1, c2) - }) -} diff --git a/wsnet/conn.go b/wsnet/conn.go deleted file mode 100644 index 40fa50ae..00000000 --- a/wsnet/conn.go +++ /dev/null @@ -1,200 +0,0 @@ -package wsnet - -import ( - "context" - "errors" - "fmt" - "net" - "net/http" - "net/url" - "sync" - "time" - - "github.com/pion/datachannel" - "github.com/pion/webrtc/v3" - "nhooyr.io/websocket" - - "cdr.dev/coder-cli/coder-sdk" -) - -const ( - httpScheme = "http" - turnProxyMagicUsername = "~magicalusername~" - - bufferedAmountLowThreshold uint64 = 512 * 1024 // 512 KB - maxBufferedAmount uint64 = 1024 * 1024 // 1 MB - // For some reason messages larger just don't work... - // This shouldn't be a huge deal for real-world usage. - // See: https://github.com/pion/datachannel/issues/59 - maxMessageLength = 32 * 1024 // 32 KB -) - -// ListenEndpoint returns the Coder endpoint to listen for workspace connections. -func ListenEndpoint(baseURL *url.URL, token string) string { - wsScheme := "wss" - if baseURL.Scheme == httpScheme { - wsScheme = "ws" - } - return fmt.Sprintf("%s://%s%s?service_token=%s", wsScheme, baseURL.Host, "/api/private/envagent/listen", token) -} - -// ConnectEndpoint returns the Coder endpoint to dial a connection for a workspace. -func ConnectEndpoint(baseURL *url.URL, workspace, token string) string { - wsScheme := "wss" - if baseURL.Scheme == httpScheme { - wsScheme = "ws" - } - return fmt.Sprintf("%s://%s%s%s%s%s", wsScheme, baseURL.Host, "/api/private/envagent/", workspace, "/connect?session_token=", token) -} - -// TURNWebSocketICECandidate returns a fake TCP relay ICEServer. -// It's used to trigger the ICEProxyDialer. -func TURNProxyICECandidate() webrtc.ICEServer { - return webrtc.ICEServer{ - URLs: []string{"turn:127.0.0.1:3478?transport=tcp"}, - Username: turnProxyMagicUsername, - Credential: turnProxyMagicUsername, - CredentialType: webrtc.ICECredentialTypePassword, - } -} - -// Proxies all TURN ICEServer traffic through this dialer. -// References Coder APIs with a specific token. -type turnProxyDialer struct { - baseURL *url.URL - token string -} - -func (t *turnProxyDialer) Dial(network, addr string) (c net.Conn, err error) { - headers := http.Header{} - headers.Set("Session-Token", t.token) - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) - defer cancel() - - // Copy the baseURL so we can adjust path. - url := *t.baseURL - switch url.Scheme { - case "http": - url.Scheme = "ws" - case "https": - url.Scheme = "wss" - default: - return nil, errors.New("invalid turn url addr scheme provided") - } - url.Path = "/api/private/turn" - conn, resp, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{ - HTTPHeader: headers, - }) - if err != nil { - if resp != nil { - defer resp.Body.Close() - return nil, coder.NewHTTPError(resp) - } - return nil, fmt.Errorf("dial: %w", err) - } - - return &turnProxyConn{ - websocket.NetConn(context.Background(), conn, websocket.MessageBinary), - }, nil -} - -// turnProxyConn is a net.Conn wrapper that returns a TCPAddr for the -// LocalAddr function. pion/ice unsafely checks the types. See: -// https://github.com/pion/ice/blob/e78f26fb435987420546c70369ade5d713beca39/gather.go#L448 -type turnProxyConn struct { - net.Conn -} - -// The LocalAddr specified here doesn't really matter, -// it just has to be of type "TCPAddr". -func (*turnProxyConn) LocalAddr() net.Addr { - return &net.TCPAddr{ - IP: net.IPv4(127, 0, 0, 1), - Port: 0, - } -} - -// Properly buffers data for data channel connections. -type dataChannelConn struct { - addr *net.UnixAddr - dc *webrtc.DataChannel - rw datachannel.ReadWriteCloser - - sendMore chan struct{} - closedMutex sync.RWMutex - closed bool - - writeMutex sync.Mutex -} - -func (c *dataChannelConn) init() { - c.closedMutex.Lock() - defer c.closedMutex.Unlock() - c.sendMore = make(chan struct{}, 1) - c.dc.SetBufferedAmountLowThreshold(bufferedAmountLowThreshold) - c.dc.OnBufferedAmountLow(func() { - c.closedMutex.RLock() - defer c.closedMutex.RUnlock() - if c.closed { - return - } - select { - case c.sendMore <- struct{}{}: - default: - } - }) -} - -func (c *dataChannelConn) Read(b []byte) (n int, err error) { - return c.rw.Read(b) -} - -func (c *dataChannelConn) Write(b []byte) (n int, err error) { - c.writeMutex.Lock() - defer c.writeMutex.Unlock() - if len(b) > maxMessageLength { - return 0, fmt.Errorf("outbound packet larger than maximum message size: %d", maxMessageLength) - } - if c.dc.BufferedAmount()+uint64(len(b)) >= maxBufferedAmount { - <-c.sendMore - } - // TODO (@kyle): There's an obvious race-condition here. - // This is an edge-case, as most-frequently data won't - // be pooled so synchronously, but is definitely possible. - // - // See: https://github.com/pion/sctp/issues/181 - time.Sleep(time.Microsecond) - - return c.rw.Write(b) -} - -func (c *dataChannelConn) Close() error { - c.closedMutex.Lock() - defer c.closedMutex.Unlock() - if !c.closed { - c.closed = true - close(c.sendMore) - } - return c.dc.Close() -} - -func (c *dataChannelConn) LocalAddr() net.Addr { - return c.addr -} - -func (c *dataChannelConn) RemoteAddr() net.Addr { - return c.addr -} - -func (c *dataChannelConn) SetDeadline(t time.Time) error { - return nil -} - -func (c *dataChannelConn) SetReadDeadline(t time.Time) error { - return nil -} - -func (c *dataChannelConn) SetWriteDeadline(t time.Time) error { - return nil -} diff --git a/wsnet/dial.go b/wsnet/dial.go deleted file mode 100644 index 8b7f4a56..00000000 --- a/wsnet/dial.go +++ /dev/null @@ -1,454 +0,0 @@ -package wsnet - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net" - "net/url" - "sync" - "time" - - "github.com/pion/datachannel" - "github.com/pion/webrtc/v3" - "golang.org/x/net/proxy" - "golang.org/x/xerrors" - "nhooyr.io/websocket" - - "cdr.dev/slog" - - "cdr.dev/coder-cli/coder-sdk" -) - -// DialOptions are configurable options for a wsnet connection. -type DialOptions struct { - // Logger is an optional logger to use for logging mostly debug messages. If - // set to nil, nothing will be logged. - Log *slog.Logger - - // ICEServers is an array of STUN or TURN servers to use for negotiation purposes. - // See: https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration/iceServers - ICEServers []webrtc.ICEServer - - // TURNProxyAuthToken is used to authenticate a TURN proxy request. - TURNProxyAuthToken string - - // TURNRemoteProxyURL is the URL to proxy listener TURN data through. - TURNRemoteProxyURL *url.URL - - // TURNLocalProxyURL is the URL to proxy client TURN data through. - TURNLocalProxyURL *url.URL -} - -// DialWebsocket dials the broker with a WebSocket and negotiates a connection. -func DialWebsocket(ctx context.Context, broker string, netOpts *DialOptions, wsOpts *websocket.DialOptions) (*Dialer, error) { - if netOpts == nil { - netOpts = &DialOptions{} - } - if netOpts.Log == nil { - // This logger will log nothing. - log := slog.Make() - netOpts.Log = &log - } - log := *netOpts.Log - - log.Debug(ctx, "connecting to broker", slog.F("broker", broker)) - conn, resp, err := websocket.Dial(ctx, broker, wsOpts) - if err != nil { - if resp != nil { - defer func() { - _ = resp.Body.Close() - }() - return nil, coder.NewHTTPError(resp) - } - return nil, fmt.Errorf("dial websocket: %w", err) - } - log.Debug(ctx, "connected to broker") - - nconn := websocket.NetConn(ctx, conn, websocket.MessageBinary) - defer func() { - _ = nconn.Close() - // We should close the socket intentionally. - _ = conn.Close(websocket.StatusInternalError, "an error occurred") - }() - return Dial(ctx, nconn, netOpts) -} - -// Dial negotiates a connection to a listener. -func Dial(ctx context.Context, conn net.Conn, options *DialOptions) (*Dialer, error) { - if options == nil { - options = &DialOptions{} - } - if options.Log == nil { - // This logger will log nothing. - log := slog.Make() - options.Log = &log - } - log := *options.Log - if options.ICEServers == nil { - options.ICEServers = []webrtc.ICEServer{} - } - - var turnProxy proxy.Dialer - if options.TURNLocalProxyURL != nil { - turnProxy = &turnProxyDialer{ - baseURL: options.TURNLocalProxyURL, - token: options.TURNProxyAuthToken, - } - } - - log.Debug(ctx, "creating peer connection", slog.F("options", options), slog.F("turn_proxy", turnProxy)) - rtc, err := newPeerConnection(options.ICEServers, turnProxy) - if err != nil { - return nil, fmt.Errorf("create peer connection: %w", err) - } - log.Debug(ctx, "created peer connection") - defer func() { - if err != nil { - // Wrap our error with some extra details. - err = errWrap{ - err: err, - iceServers: rtc.GetConfiguration().ICEServers, - rtc: rtc.ConnectionState(), - } - - closeErr := rtc.Close() - if closeErr != nil { - log.Warn(context.Background(), "close rtc connection on dial failure", slog.Error(closeErr)) - } - } - }() - - rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { - log.Debug(ctx, "connection state change", slog.F("state", pcs.String())) - }) - - flushCandidates := proxyICECandidates(rtc, conn) - - log.Debug(ctx, "creating control channel", slog.F("proto", controlChannel)) - ctrl, err := rtc.CreateDataChannel(controlChannel, &webrtc.DataChannelInit{ - Protocol: stringPtr(controlChannel), - Ordered: boolPtr(true), - }) - if err != nil { - return nil, fmt.Errorf("create control channel: %w", err) - } - - offer, err := rtc.CreateOffer(&webrtc.OfferOptions{}) - if err != nil { - return nil, fmt.Errorf("create offer: %w", err) - } - log.Debug(ctx, "created offer", slog.F("offer", offer)) - err = rtc.SetLocalDescription(offer) - if err != nil { - return nil, fmt.Errorf("set local offer: %w", err) - } - - var turnProxyURL string - if options.TURNRemoteProxyURL != nil { - turnProxyURL = options.TURNRemoteProxyURL.String() - } - - bmsg := BrokerMessage{ - Offer: &offer, - Servers: options.ICEServers, - TURNProxyURL: turnProxyURL, - } - log.Debug(ctx, "sending offer message", slog.F("msg", bmsg)) - offerMessage, err := json.Marshal(&bmsg) - if err != nil { - return nil, fmt.Errorf("marshal offer message: %w", err) - } - - _, err = conn.Write(offerMessage) - if err != nil { - return nil, fmt.Errorf("write offer: %w", err) - } - flushCandidates() - - dialer := &Dialer{ - log: log, - conn: conn, - ctrl: ctrl, - rtc: rtc, - connClosers: []io.Closer{ctrl}, - } - - err = dialer.negotiate(ctx) - if err != nil { - // Return the dialer since we have tests that verify things are closed - // if negotiation fails. - return dialer, xerrors.Errorf("negotiate rtc connection: %w", err) - } - - return dialer, nil -} - -// Dialer enables arbitrary dialing to any network and address -// inside a workspace. The opposing end of the WebSocket messages -// should be proxied with a Listener. -type Dialer struct { - log slog.Logger - conn net.Conn - ctrl *webrtc.DataChannel - ctrlrw datachannel.ReadWriteCloser - rtc *webrtc.PeerConnection - - connClosers []io.Closer - connClosersMut sync.Mutex - pingMut sync.Mutex -} - -func (d *Dialer) negotiate(ctx context.Context) (err error) { - var ( - decoder = json.NewDecoder(d.conn) - errCh = make(chan error, 1) - // If candidates are sent before an offer, we place them here. - // We currently have no assurances to ensure this can't happen, - // so it's better to buffer and process than fail. - pendingCandidates = []webrtc.ICECandidateInit{} - ) - go func() { - defer close(errCh) - defer func() { _ = d.conn.Close() }() - - err := waitForConnectionOpen(context.Background(), d.rtc) - if err != nil { - d.log.Debug(ctx, "negotiation error", slog.Error(err)) - - errCh <- fmt.Errorf("wait for connection to open: %w", err) - return - } - - d.rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { - if pcs == webrtc.PeerConnectionStateConnected { - d.log.Debug(ctx, "connected") - return - } - - // Close connections opened when RTC was alive. - d.log.Warn(ctx, "closing connections due to connection state change", slog.F("pcs", pcs.String())) - d.connClosersMut.Lock() - defer d.connClosersMut.Unlock() - for _, connCloser := range d.connClosers { - _ = connCloser.Close() - } - d.connClosers = make([]io.Closer, 0) - }) - }() - - d.log.Debug(ctx, "beginning negotiation") - for { - var msg BrokerMessage - err = decoder.Decode(&msg) - if errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe) { - break - } - if err != nil { - return fmt.Errorf("read: %w", err) - } - d.log.Debug(ctx, "got message from handshake conn", slog.F("msg", msg)) - - if msg.Candidate != "" { - c := webrtc.ICECandidateInit{ - Candidate: msg.Candidate, - } - if d.rtc.RemoteDescription() == nil { - pendingCandidates = append(pendingCandidates, c) - continue - } - - d.log.Debug(ctx, "adding remote ICE candidate", slog.F("c", c)) - err = d.rtc.AddICECandidate(c) - if err != nil { - return fmt.Errorf("accept ice candidate: %s: %w", msg.Candidate, err) - } - continue - } - - if msg.Answer != nil { - d.log.Debug(ctx, "received answer", slog.F("a", *msg.Answer)) - err = d.rtc.SetRemoteDescription(*msg.Answer) - if err != nil { - return fmt.Errorf("set answer: %w", err) - } - - for _, candidate := range pendingCandidates { - err = d.rtc.AddICECandidate(candidate) - if err != nil { - return fmt.Errorf("accept pending ice candidate: %s: %w", candidate.Candidate, err) - } - } - pendingCandidates = nil - continue - } - - if msg.Error != "" { - d.log.Debug(ctx, "got error from peer", slog.F("err", msg.Error)) - return fmt.Errorf("error from peer: %v", msg.Error) - } - - return fmt.Errorf("unhandled message: %+v", msg) - } - - return <-errCh -} - -// ActiveConnections returns the amount of active connections. DialContext -// opens a connection, and close will end it. -func (d *Dialer) activeConnections() int { - stats, ok := d.rtc.GetStats().GetConnectionStats(d.rtc) - if !ok { - return -1 - } - // Subtract 1 for the control channel. - return int(stats.DataChannelsRequested-stats.DataChannelsClosed) - 1 -} - -// Candidates returns the candidate pair that was chosen for the connection. -func (d *Dialer) Candidates() (*webrtc.ICECandidatePair, error) { - return d.rtc.SCTP().Transport().ICETransport().GetSelectedCandidatePair() -} - -// Close closes the RTC connection. -// All data channels dialed will be closed. -func (d *Dialer) Close() error { - d.log.Debug(context.Background(), "close called") - return d.rtc.Close() -} - -// Ping sends a ping through the control channel. -func (d *Dialer) Ping(ctx context.Context) error { - if d.ctrl.ReadyState() == webrtc.DataChannelStateClosed || d.ctrl.ReadyState() == webrtc.DataChannelStateClosing { - return webrtc.ErrConnectionClosed - } - - // Since we control the client and server we could open this - // data channel with `Negotiated` true to reduce traffic being - // sent when the RTC connection is opened. - err := waitForDataChannelOpen(ctx, d.ctrl) - if err != nil { - return err - } - if d.ctrlrw == nil { - d.ctrlrw, err = d.ctrl.Detach() - if err != nil { - return err - } - } - - d.pingMut.Lock() - defer d.pingMut.Unlock() - - d.log.Debug(ctx, "sending ping") - _, err = d.ctrlrw.Write([]byte{'a'}) - if err != nil { - return fmt.Errorf("write: %w", err) - } - - errCh := make(chan error, 1) - go func() { - // There's a race in which connections can get lost-mid ping - // in which case this would block forever. - defer close(errCh) - _, err = d.ctrlrw.Read(make([]byte, 4)) - errCh <- err - }() - - ctx, cancel := context.WithTimeout(ctx, time.Second*15) - defer cancel() - - select { - case err := <-errCh: - return err - case <-ctx.Done(): - return ctx.Err() - } -} - -// DialContext dials the network and address on the remote listener. -func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - proto := fmt.Sprintf("%s:%s", network, address) - ctx = slog.With(ctx, slog.F("proto", proto)) - - d.log.Debug(ctx, "opening data channel") - dc, err := d.rtc.CreateDataChannel("proxy", &webrtc.DataChannelInit{ - Ordered: boolPtr(network != "udp"), - Protocol: &proto, - }) - if err != nil { - return nil, fmt.Errorf("create data channel: %w", err) - } - - d.connClosersMut.Lock() - d.connClosers = append(d.connClosers, dc) - d.connClosersMut.Unlock() - - err = waitForDataChannelOpen(ctx, dc) - if err != nil { - return nil, fmt.Errorf("wait for open: %w", err) - } - - ctx = slog.With(ctx, slog.F("dc_id", dc.ID())) - d.log.Debug(ctx, "data channel opened") - - rw, err := dc.Detach() - if err != nil { - return nil, fmt.Errorf("detach: %w", err) - } - d.log.Debug(ctx, "data channel detached") - - ctx, cancel := context.WithTimeout(ctx, time.Second*5) - defer cancel() - - errCh := make(chan error, 1) - go func() { - defer close(errCh) - - var res DialChannelResponse - err = json.NewDecoder(rw).Decode(&res) - if err != nil { - errCh <- fmt.Errorf("read dial response: %w", err) - return - } - - d.log.Debug(ctx, "dial response", slog.F("res", res)) - if res.Err == "" { - return - } - - err := errors.New(res.Err) - if res.Code == CodeDialErr { - err = &net.OpError{ - Op: res.Op, - Net: res.Net, - Err: err, - } - } - errCh <- err - }() - - select { - case err := <-errCh: - if err != nil { - return nil, err - } - case <-ctx.Done(): - return nil, ctx.Err() - } - - c := &dataChannelConn{ - addr: &net.UnixAddr{ - Name: address, - Net: network, - }, - dc: dc, - rw: rw, - } - c.init() - - d.log.Debug(ctx, "dial channel ready") - return c, nil -} diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go deleted file mode 100644 index d57559c3..00000000 --- a/wsnet/dial_test.go +++ /dev/null @@ -1,425 +0,0 @@ -package wsnet - -import ( - "context" - "crypto/rand" - "errors" - "fmt" - "io" - "net" - "strconv" - "testing" - "time" - - "github.com/pion/ice/v2" - "github.com/pion/webrtc/v3" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "cdr.dev/slog/sloggers/slogtest" -) - -func ExampleDial_basic() { - servers := []webrtc.ICEServer{{ - URLs: []string{"turns:master.cdr.dev"}, - Username: "kyle", - Credential: "pass", - CredentialType: webrtc.ICECredentialTypePassword, - }} - - for _, server := range servers { - err := DialICE(server, nil) - if errors.Is(err, ErrInvalidCredentials) { - // You could do something... - } - if errors.Is(err, ErrMismatchedProtocol) { - // Likely they used TURNS when they should have used TURN. - // Or they could have used TURN instead of TURNS. - } - } - - dialer, err := DialWebsocket(context.Background(), "wss://master.cdr.dev/agent/workspace/connect", &DialOptions{ - ICEServers: servers, - }, nil) - if err != nil { - // Do something... - } - conn, err := dialer.DialContext(context.Background(), "tcp", "localhost:13337") - if err != nil { - // Something... - } - defer conn.Close() - // You now have access to the proxied remote port in `conn`. -} - -func TestDial(t *testing.T) { - t.Run("Timeout", func(t *testing.T) { - t.Parallel() - - connectAddr, _ := createDumbBroker(t) - - ctx, cancelFunc := context.WithTimeout(context.Background(), time.Millisecond*50) - defer cancelFunc() - dialer, err := DialWebsocket(ctx, connectAddr, nil, nil) - require.True(t, errors.Is(err, context.DeadlineExceeded)) - require.NotNil(t, dialer) - require.Error(t, dialer.conn.Close(), "already wrote close") - - // Ensure the rtc peer connection is closed. Setting the config options - // to empty struct does nothing, but it does fail if the rtc peer conn - // is closed. - err = dialer.rtc.SetConfiguration(webrtc.Configuration{}) - require.Error(t, err) - require.ErrorIs(t, err, webrtc.ErrConnectionClosed) - }) - - t.Run("Ping", func(t *testing.T) { - t.Parallel() - log := slogtest.Make(t, nil) - - connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), log, listenAddr, "") - require.NoError(t, err) - defer l.Close() - - dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ - Log: &log, - }, nil) - require.NoError(t, err) - - err = dialer.Ping(context.Background()) - require.NoError(t, err) - }) - - t.Run("Ping Close", func(t *testing.T) { - t.Parallel() - log := slogtest.Make(t, nil) - - connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), log, listenAddr, "") - require.NoError(t, err) - defer l.Close() - - turnAddr, closeTurn := createTURNServer(t, ice.SchemeTypeTURN) - dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ - Log: &log, - ICEServers: []webrtc.ICEServer{{ - URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, - Username: "example", - Credential: testPass, - CredentialType: webrtc.ICECredentialTypePassword, - }}, - }, nil) - require.NoError(t, err) - - _ = dialer.Ping(context.Background()) - closeTurn() - err = dialer.Ping(context.Background()) - assert.Error(t, err) - assert.ErrorIs(t, err, io.EOF) - }) - - t.Run("OPError", func(t *testing.T) { - t.Parallel() - log := slogtest.Make(t, nil) - - connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), log, listenAddr, "") - require.NoError(t, err) - defer l.Close() - - dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ - Log: &log, - }, nil) - require.NoError(t, err) - - _, err = dialer.DialContext(context.Background(), "tcp", "localhost:100") - assert.Error(t, err) - - // Double pointer intended. - netErr := &net.OpError{} - assert.ErrorAs(t, err, &netErr) - }) - - t.Run("Proxy", func(t *testing.T) { - t.Parallel() - log := slogtest.Make(t, nil) - - listener, err := net.Listen("tcp", "0.0.0.0:0") - require.NoError(t, err) - - msg := []byte("Hello!") - go func() { - conn, err := listener.Accept() - require.NoError(t, err) - - _, _ = conn.Write(msg) - }() - - connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), log, listenAddr, "") - require.NoError(t, err) - defer l.Close() - - dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ - Log: &log, - }, nil) - require.NoError(t, err) - - conn, err := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) - require.NoError(t, err) - - rec := make([]byte, len(msg)) - _, err = conn.Read(rec) - require.NoError(t, err) - - assert.Equal(t, msg, rec) - }) - - // Expect that we'd get an EOF on the server closing. - t.Run("EOF on Close", func(t *testing.T) { - t.Parallel() - log := slogtest.Make(t, nil) - - listener, err := net.Listen("tcp", "0.0.0.0:0") - require.NoError(t, err) - go func() { - _, _ = listener.Accept() - }() - - connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), log, listenAddr, "") - require.NoError(t, err) - defer l.Close() - - dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ - Log: &log, - }, nil) - require.NoError(t, err) - - conn, err := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) - require.NoError(t, err) - - go l.Close() - rec := make([]byte, 16) - _, err = conn.Read(rec) - assert.ErrorIs(t, err, io.EOF) - }) - - t.Run("Disconnect", func(t *testing.T) { - t.Parallel() - log := slogtest.Make(t, nil) - - connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), log, listenAddr, "") - require.NoError(t, err) - defer l.Close() - - dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ - Log: &log, - }, nil) - require.NoError(t, err) - - err = dialer.Close() - require.NoError(t, err) - - err = dialer.Ping(context.Background()) - assert.ErrorIs(t, err, webrtc.ErrConnectionClosed) - }) - - t.Run("Disconnect DialContext", func(t *testing.T) { - t.Parallel() - log := slogtest.Make(t, nil) - - tcpListener, err := net.Listen("tcp", "0.0.0.0:0") - require.NoError(t, err) - go func() { - _, _ = tcpListener.Accept() - }() - - connectAddr, listenAddr := createDumbBroker(t) - l, err := Listen(context.Background(), log, listenAddr, "") - require.NoError(t, err) - defer l.Close() - - turnAddr, closeTurn := createTURNServer(t, ice.SchemeTypeTURN) - dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ - Log: &log, - ICEServers: []webrtc.ICEServer{{ - URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, - Username: "example", - Credential: testPass, - CredentialType: webrtc.ICECredentialTypePassword, - }}, - }, nil) - require.NoError(t, err) - - conn, err := dialer.DialContext(context.Background(), "tcp", tcpListener.Addr().String()) - require.NoError(t, err) - - // Close the TURN server before reading... - // WebRTC connections take a few seconds to timeout. - closeTurn() - _, err = conn.Read(make([]byte, 16)) - assert.ErrorIs(t, err, io.EOF) - }) - - t.Run("Active Connections", func(t *testing.T) { - t.Parallel() - log := slogtest.Make(t, nil) - - listener, err := net.Listen("tcp", "0.0.0.0:0") - require.NoError(t, err) - - go func() { - _, _ = listener.Accept() - }() - - connectAddr, listenAddr := createDumbBroker(t) - _, err = Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") - require.NoError(t, err) - - dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ - Log: &log, - }, nil) - require.NoError(t, err) - - conn, err := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) - require.NoError(t, err) - assert.Equal(t, 1, dialer.activeConnections()) - - _ = conn.Close() - assert.Equal(t, 0, dialer.activeConnections()) - - _, err = dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) - require.NoError(t, err) - - conn, err = dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) - require.NoError(t, err) - assert.Equal(t, 2, dialer.activeConnections()) - - _ = conn.Close() - assert.Equal(t, 1, dialer.activeConnections()) - }) - - t.Run("Close Listeners on Disconnect", func(t *testing.T) { - t.Parallel() - log := slogtest.Make(t, nil) - - listener, err := net.Listen("tcp", "0.0.0.0:0") - require.NoError(t, err) - go func() { - for { - c, _ := listener.Accept() - - go func() { - b := make([]byte, 5) - _, err := c.Read(b) - if err != nil { - return - } - _, err = c.Write(b) - require.NoError(t, err) - }() - } - }() - connectAddr, listenAddr := createDumbBroker(t) - _, err = Listen(context.Background(), slogtest.Make(t, nil), listenAddr, "") - require.NoError(t, err) - - d1, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ - Log: &log, - }, nil) - require.NoError(t, err) - _, err = d1.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) - require.NoError(t, err) - - d2, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ - Log: &log, - }, nil) - require.NoError(t, err) - conn, err := d2.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) - require.NoError(t, err) - err = d1.Close() - require.NoError(t, err) - - // TODO: This needs to be longer than the KeepAlive timeout for the RTC connection. - // Once the listener stores RTC connections instead of io.Closer we can directly - // reference the RTC connection to ensure it's properly closed. - time.Sleep(time.Second * 10) - - b := []byte("hello") - _, err = conn.Write(b) - require.NoError(t, err) - _, err = conn.Read(b) - require.NoError(t, err) - }) -} - -func BenchmarkThroughput(b *testing.B) { - sizes := []int64{ - 4, - 16, - 128, - 256, - 1024, - 4096, - 16384, - 32768, - } - - listener, err := net.Listen("tcp", "0.0.0.0:0") - if err != nil { - b.Error(err) - return - } - go func() { - for { - conn, err := listener.Accept() - if err != nil { - b.Error(err) - return - } - go func() { - _, _ = io.Copy(io.Discard, conn) - }() - } - }() - connectAddr, listenAddr := createDumbBroker(b) - l, err := Listen(context.Background(), slogtest.Make(b, nil), listenAddr, "") - if err != nil { - b.Error(err) - return - } - defer l.Close() - - dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) - if err != nil { - b.Error(err) - return - } - for _, size := range sizes { - size := size - bytes := make([]byte, size) - _, _ = rand.Read(bytes) - b.Run("Rand"+strconv.Itoa(int(size)), func(b *testing.B) { - b.SetBytes(size) - b.ReportAllocs() - - conn, err := dialer.DialContext(context.Background(), listener.Addr().Network(), listener.Addr().String()) - if err != nil { - b.Error(err) - return - } - defer conn.Close() - - for i := 0; i < b.N; i++ { - _, err := conn.Write(bytes) - if err != nil { - b.Error(err) - break - } - } - }) - } -} diff --git a/wsnet/doc.go b/wsnet/doc.go deleted file mode 100644 index 3cbdc3ce..00000000 --- a/wsnet/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package wsnet handles client and server ends of Workspace networking -// negotiations and protocol. -package wsnet diff --git a/wsnet/error.go b/wsnet/error.go deleted file mode 100644 index aa66d548..00000000 --- a/wsnet/error.go +++ /dev/null @@ -1,39 +0,0 @@ -package wsnet - -import ( - "fmt" - "strings" - - "github.com/pion/webrtc/v3" -) - -// errWrap wraps the error with some extra details about the state of the -// connection. -type errWrap struct { - err error - - iceServers []webrtc.ICEServer - rtc webrtc.PeerConnectionState -} - -var _ error = errWrap{} -var _ interface{ Unwrap() error } = errWrap{} - -// Error implements error. -func (e errWrap) Error() string { - return fmt.Sprintf("%v (ice: [%v], rtc: %v)", e.err.Error(), e.ice(), e.rtc.String()) -} - -func (e errWrap) ice() string { - msgs := []string{} - for _, s := range e.iceServers { - msgs = append(msgs, strings.Join(s.URLs, ", ")) - } - - return strings.Join(msgs, ", ") -} - -// Unwrap implements Unwrapper. -func (e errWrap) Unwrap() error { - return e.err -} diff --git a/wsnet/listen.go b/wsnet/listen.go deleted file mode 100644 index 6bee01c1..00000000 --- a/wsnet/listen.go +++ /dev/null @@ -1,479 +0,0 @@ -package wsnet - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net" - "net/url" - "sync" - "sync/atomic" - "time" - - "github.com/hashicorp/yamux" - "github.com/pion/webrtc/v3" - "golang.org/x/net/proxy" - "nhooyr.io/websocket" - - "cdr.dev/slog" - - "cdr.dev/coder-cli/coder-sdk" -) - -// Codes for DialChannelResponse. -const ( - CodeDialErr = "dial_error" - CodePermissionErr = "permission_error" - CodeBadAddressErr = "bad_address_error" -) - -var connectionRetryInterval = time.Second - -// DialChannelResponse is used to notify a dial channel of a -// listening state. Modeled after net.OpError, and marshalled -// to that if Net is not "". -type DialChannelResponse struct { - Code string - Err string - // Fields are set if the code is CodeDialErr. - Net string - Op string -} - -// Listen connects to the broker proxies connections to the local net. -// Close will end all RTC connections. -func Listen(ctx context.Context, log slog.Logger, broker string, turnProxyAuthToken string) (io.Closer, error) { - l := &listener{ - log: log, - broker: broker, - connClosers: make([]io.Closer, 0), - closed: make(chan struct{}, 1), - turnProxyAuthToken: turnProxyAuthToken, - } - - // We do a one-off dial outside of the loop to ensure the initial - // connection is successful. If not, there's likely an error the - // user needs to act on. - ch, err := l.dial(ctx) - if err != nil { - return nil, err - } - go func() { - for { - err := <-ch - select { - case _, ok := <-l.closed: - if !ok { - return - } - default: - } - - if err != nil { - l.log.Warn(ctx, "disconnected from broker", slog.Error(err)) - - // If we hit an EOF, then the connection to the broker - // was interrupted. We'll take a short break then dial - // again. - ticker := time.NewTicker(connectionRetryInterval) - for { - select { - case <-ticker.C: - ch, err = l.dial(ctx) - case <-ctx.Done(): - err = ctx.Err() - } - if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - break - } - l.log.Warn(ctx, "connecting to broker failed", slog.Error(err)) - } - ticker.Stop() - } - l.log.Info(ctx, "connected to broker") - } - }() - return l, nil -} - -type listener struct { - broker string - turnProxyAuthToken string - - log slog.Logger - ws *websocket.Conn - connClosers []io.Closer - connClosersMut sync.Mutex - closed chan struct{} - nextConnNumber int64 -} - -func (l *listener) dial(ctx context.Context) (<-chan error, error) { - l.log.Info(ctx, "connecting to broker", slog.F("broker_url", l.broker)) - if l.ws != nil { - _ = l.ws.Close(websocket.StatusNormalClosure, "new connection inbound") - } - - // websocket lib documents that the response does not need to be closed. - // nolint - conn, resp, err := websocket.Dial(ctx, l.broker, nil) - if err != nil { - if resp != nil { - return nil, coder.NewHTTPError(resp) - } - return nil, err - } - l.ws = conn - - nconn := websocket.NetConn(ctx, conn, websocket.MessageBinary) - config := yamux.DefaultConfig() - config.LogOutput = io.Discard - session, err := yamux.Server(nconn, config) - if err != nil { - return nil, fmt.Errorf("create multiplex: %w", err) - } - - l.log.Info(ctx, "broker connection established") - errCh := make(chan error) - go func() { - defer close(errCh) - for { - conn, err := session.Accept() - if err != nil { - errCh <- err - break - } - go l.negotiate(ctx, conn) - } - }() - - return errCh, nil -} - -// Negotiates the handshake protocol over the connection provided. -// This functions control-flow is important to readability, -// so the cognitive overload linter has been disabled. -// nolint:gocognit,nestif -func (l *listener) negotiate(ctx context.Context, conn net.Conn) { - id := atomic.AddInt64(&l.nextConnNumber, 1) - ctx = slog.With(ctx, slog.F("conn_id", id)) - - var ( - err error - decoder = json.NewDecoder(conn) - rtc *webrtc.PeerConnection - connClosers = make([]io.Closer, 0) - connClosersMut sync.Mutex - // If candidates are sent before an offer, we place them here. - // We currently have no assurances to ensure this can't happen, - // so it's better to buffer and process than fail. - pendingCandidates = []webrtc.ICECandidateInit{} - // Sends the error provided then closes the connection. - // If RTC isn't connected, we'll close it. - closeError = func(err error) { - // l.log.Warn(ctx, "negotiation error, closing connection", slog.Error(err)) - - d, _ := json.Marshal(&BrokerMessage{ - Error: err.Error(), - }) - _, _ = conn.Write(d) - _ = conn.Close() - if rtc != nil { - if rtc.ConnectionState() != webrtc.PeerConnectionStateConnected { - rtc.Close() - rtc = nil - } - } - } - ) - - l.log.Info(ctx, "accepted new session from broker connection, negotiating") - - for { - var msg BrokerMessage - err = decoder.Decode(&msg) - if err != nil { - closeError(err) - return - } - l.log.Debug(ctx, "received broker message", slog.F("msg", msg)) - - if msg.Candidate != "" { - c := webrtc.ICECandidateInit{ - Candidate: msg.Candidate, - } - - if rtc == nil { - pendingCandidates = append(pendingCandidates, c) - continue - } - - l.log.Debug(ctx, "adding ICE candidate", slog.F("c", c)) - err = rtc.AddICECandidate(c) - if err != nil { - closeError(fmt.Errorf("accept ice candidate: %w", err)) - return - } - } - - if msg.Offer != nil { - if msg.Servers == nil { - closeError(fmt.Errorf("ICEServers must be provided")) - return - } - for _, server := range msg.Servers { - if server.Username == turnProxyMagicUsername { - // This candidate is only used when proxying, - // so it will not validate. - continue - } - - l.log.Debug(ctx, "validating ICE server", slog.F("s", server)) - err = DialICE(server, nil) - if err != nil { - closeError(fmt.Errorf("dial server %+v: %w", server.URLs, err)) - return - } - } - - var turnProxy proxy.Dialer - if msg.TURNProxyURL != "" { - u, err := url.Parse(msg.TURNProxyURL) - if err != nil { - closeError(fmt.Errorf("parse turn proxy url: %w", err)) - return - } - turnProxy = &turnProxyDialer{ - baseURL: u, - token: l.turnProxyAuthToken, - } - } - rtc, err = newPeerConnection(msg.Servers, turnProxy) - if err != nil { - closeError(err) - return - } - l.connClosersMut.Lock() - l.connClosers = append(l.connClosers, rtc) - l.connClosersMut.Unlock() - rtc.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { - l.log.Info(ctx, "connection state change", slog.F("state", pcs.String())) - switch pcs { - case webrtc.PeerConnectionStateConnected: - return - case webrtc.PeerConnectionStateConnecting: - // Safe to close the negotiating WebSocket. - _ = conn.Close() - return - } - - // Close connections opened when RTC was alive. - connClosersMut.Lock() - defer connClosersMut.Unlock() - for _, connCloser := range connClosers { - _ = connCloser.Close() - } - connClosers = make([]io.Closer, 0) - }) - - flushCandidates := proxyICECandidates(rtc, conn) - rtc.OnDataChannel(l.handle(ctx, msg, &connClosers, &connClosersMut)) - - l.log.Debug(ctx, "set remote description", slog.F("offer", *msg.Offer)) - err = rtc.SetRemoteDescription(*msg.Offer) - if err != nil { - closeError(fmt.Errorf("apply offer: %w", err)) - return - } - - answer, err := rtc.CreateAnswer(nil) - if err != nil { - closeError(fmt.Errorf("create answer: %w", err)) - return - } - - l.log.Debug(ctx, "set local description", slog.F("answer", answer)) - err = rtc.SetLocalDescription(answer) - if err != nil { - closeError(fmt.Errorf("set local answer: %w", err)) - return - } - flushCandidates() - - bmsg := &BrokerMessage{ - Answer: rtc.LocalDescription(), - } - data, err := json.Marshal(bmsg) - if err != nil { - closeError(fmt.Errorf("marshal: %w", err)) - return - } - - l.log.Debug(ctx, "writing message", slog.F("msg", bmsg)) - _, err = conn.Write(data) - if err != nil { - closeError(fmt.Errorf("write: %w", err)) - return - } - - for _, candidate := range pendingCandidates { - l.log.Debug(ctx, "adding pending ICE candidate", slog.F("c", candidate)) - err = rtc.AddICECandidate(candidate) - if err != nil { - closeError(fmt.Errorf("add pending candidate: %w", err)) - return - } - } - pendingCandidates = nil - } - } -} - -// nolint:gocognit -func (l *listener) handle(ctx context.Context, msg BrokerMessage, connClosers *[]io.Closer, connClosersMut *sync.Mutex) func(dc *webrtc.DataChannel) { - return func(dc *webrtc.DataChannel) { - if dc.Protocol() == controlChannel { - // The control channel handles pings. - dc.OnOpen(func() { - l.log.Debug(ctx, "control channel open") - rw, err := dc.Detach() - if err != nil { - return - } - // We'll read and write back a single byte for ping/pongin'. - d := make([]byte, 1) - for { - l.log.Debug(ctx, "sending ping") - _, err = rw.Read(d) - if err != nil { - l.log.Debug(ctx, "reading ping response failed", slog.Error(err)) - } - if errors.Is(err, io.EOF) { - return - } - if err != nil { - continue - } - _, _ = rw.Write(d) - } - }) - return - } - - ctx := slog.With(ctx, - slog.F("dc_id", dc.ID()), - slog.F("dc_label", dc.Label()), - slog.F("dc_proto", dc.Protocol()), - ) - - dc.OnOpen(func() { - l.log.Info(ctx, "data channel opened") - rw, err := dc.Detach() - if err != nil { - return - } - - var init DialChannelResponse - sendInitMessage := func() { - l.log.Debug(ctx, "sending dc init message", slog.F("msg", init)) - initData, err := json.Marshal(&init) - if err != nil { - l.log.Debug(ctx, "failed to marshal dc init message", slog.Error(err)) - rw.Close() - return - } - _, err = rw.Write(initData) - if err != nil { - l.log.Debug(ctx, "failed to write dc init message", slog.Error(err)) - return - } - if init.Err != "" { - // If an error occurred, we're safe to close the connection. - l.log.Debug(ctx, "closing data channel due to error", slog.F("msg", init.Err)) - dc.Close() - return - } - } - - network, addr, err := msg.getAddress(dc.Protocol()) - if err != nil { - init.Code = CodeBadAddressErr - init.Err = err.Error() - var policyErr notPermittedByPolicyErr - if errors.As(err, &policyErr) { - init.Code = CodePermissionErr - } - sendInitMessage() - return - } - - l.log.Debug(ctx, "dialing remote address", slog.F("network", network), slog.F("addr", addr)) - nc, err := net.Dial(network, addr) - if err != nil { - l.log.Debug(ctx, "failed to dial remote address") - init.Code = CodeDialErr - init.Err = err.Error() - if op, ok := err.(*net.OpError); ok { - init.Net = op.Net - init.Op = op.Op - } - } - sendInitMessage() - if init.Err != "" { - return - } - - // Must wrap the data channel inside this connection - // for buffering from the dialed endpoint to the client. - l.log.Debug(ctx, "data channel initialized, tunnelling") - co := &dataChannelConn{ - addr: nil, - dc: dc, - rw: rw, - } - connClosersMut.Lock() - *connClosers = append(*connClosers, co) - connClosersMut.Unlock() - co.init() - defer nc.Close() - defer co.Close() - go func() { - defer dc.Close() - _, _ = io.Copy(co, nc) - }() - _, _ = io.Copy(nc, co) - }) - } -} - -// Close closes the broker socket and all created RTC connections. -func (l *listener) Close() error { - l.log.Info(context.Background(), "listener closed") - - l.connClosersMut.Lock() - defer l.connClosersMut.Unlock() - - select { - case _, ok := <-l.closed: - if !ok { - return errors.New("already closed") - } - default: - } - close(l.closed) - - for _, connCloser := range l.connClosers { - // We can ignore the error here... it doesn't - // really matter if these fail to close. - _ = connCloser.Close() - } - return l.ws.Close(websocket.StatusNormalClosure, "") -} - -// Since this listener is bound to the WebSocket, we could -// return that resolved Addr, but until we need it we won't. -func (l *listener) Addr() net.Addr { - return nil -} diff --git a/wsnet/listen_test.go b/wsnet/listen_test.go deleted file mode 100644 index 78b56691..00000000 --- a/wsnet/listen_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package wsnet - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - "time" - - "cdr.dev/slog/sloggers/slogtest" - "github.com/stretchr/testify/require" - "nhooyr.io/websocket" -) - -func init() { - // We override this value to make tests faster. - connectionRetryInterval = 10 * time.Millisecond -} - -func TestListen(t *testing.T) { - t.Run("Reconnect", func(t *testing.T) { - var ( - connCh = make(chan *websocket.Conn) - mux = http.NewServeMux() - ) - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - ws, err := websocket.Accept(w, r, nil) - if err != nil { - t.Error(err) - return - } - connCh <- ws - }) - - s := httptest.NewServer(mux) - defer s.Close() - - l, err := Listen(context.Background(), slogtest.Make(t, nil), s.URL, "") - require.NoError(t, err) - defer l.Close() - conn := <-connCh - - // Kill the server connection. - err = conn.Close(websocket.StatusGoingAway, "") - require.NoError(t, err) - - // At least a few retry attempts should be had... - time.Sleep(connectionRetryInterval * 5) - <-connCh - }) -} diff --git a/wsnet/proto.go b/wsnet/proto.go deleted file mode 100644 index feb4d126..00000000 --- a/wsnet/proto.go +++ /dev/null @@ -1,137 +0,0 @@ -package wsnet - -import ( - "fmt" - "math/bits" - "net" - "strconv" - "strings" - - "github.com/pion/webrtc/v3" -) - -// DialPolicy a single network + address + port combinations that a connection -// is permitted to use. -type DialPolicy struct { - // If network is empty, it applies to all networks. - Network string `json:"network"` - // Host is the IP or hostname of the address. It should not contain the - // port.If empty, it applies to all hosts. "localhost", [::1], and any IPv4 - // address under "127.0.0.0/8" can be used interchangeably. - Host string `json:"address"` - // If port is 0, it applies to all ports. - Port uint16 `json:"port"` -} - -// permits checks if a DialPolicy permits a specific network + host + port -// combination. The host must be put through normalizeHost first. -func (p DialPolicy) permits(network, host string, port uint16) bool { - if p.Network != "" && p.Network != network { - return false - } - if p.Host != "" && canonicalizeHost(p.Host) != host { - return false - } - if p.Port != 0 && p.Port != port { - return false - } - - return true -} - -// BrokerMessage is used for brokering a dialer and listener. -// -// Dialers initiate an exchange by providing an Offer, -// along with a list of ICE servers for the listener to -// peer with. -// -// The listener should respond with an offer, then both -// sides can begin exchanging candidates. -type BrokerMessage struct { - // Dialer -> Listener - Offer *webrtc.SessionDescription `json:"offer"` - Servers []webrtc.ICEServer `json:"servers"` - TURNProxyURL string `json:"turn_proxy_url"` - - // Policies denote which addresses the client can dial. If empty or nil, all - // addresses are permitted. - Policies []DialPolicy `json:"ports"` - - // Listener -> Dialer - Error string `json:"error"` - Answer *webrtc.SessionDescription `json:"answer"` - - // Bidirectional - Candidate string `json:"candidate"` -} - -// getAddress parses the data channel's protocol into an address suitable for -// net.Dial. It also verifies that the BrokerMessage permits connecting to said -// address. -func (msg BrokerMessage) getAddress(protocol string) (netwk, addr string, err error) { - parts := strings.SplitN(protocol, ":", 2) - if len(parts) != 2 { - return "", "", fmt.Errorf("invalid dial address: %v", protocol) - } - host, port, err := net.SplitHostPort(parts[1]) - if err != nil { - return "", "", fmt.Errorf("invalid dial address: %v", protocol) - } - - var ( - network = parts[0] - normalHost = canonicalizeHost(host) - // Still return the original host value, not the canonical value. - fullAddr = net.JoinHostPort(host, port) - ) - if network == "" { - return "", "", fmt.Errorf("invalid dial address %q network: %v", protocol, network) - } - if host == "" { - return "", "", fmt.Errorf("invalid dial address %q host: %v", protocol, host) - } - - portParsed, err := strconv.Atoi(port) - if err != nil || portParsed < 0 || bits.Len(uint(portParsed)) > 16 { - return "", "", fmt.Errorf("invalid dial address %q port: %v", protocol, port) - } - if len(msg.Policies) == 0 { - return network, fullAddr, nil - } - - portParsedU16 := uint16(portParsed) - for _, p := range msg.Policies { - if p.permits(network, normalHost, portParsedU16) { - return network, fullAddr, nil - } - } - - return "", "", fmt.Errorf("connections are not permitted to %q by policy", protocol) -} - -// canonicalizeHost converts all representations of "localhost" to "localhost". -func canonicalizeHost(addr string) string { - addr = strings.TrimPrefix(addr, "[") - addr = strings.TrimSuffix(addr, "]") - - ip := net.ParseIP(addr) - if ip == nil { - return addr - } - - if ip.IsLoopback() { - return "localhost" - } - return addr -} - -type notPermittedByPolicyErr struct { - protocol string -} - -var _ error = notPermittedByPolicyErr{} - -// Error implements error. -func (e notPermittedByPolicyErr) Error() string { - return fmt.Sprintf("connections are not permitted to %q by policy", e.protocol) -} diff --git a/wsnet/proto_test.go b/wsnet/proto_test.go deleted file mode 100644 index 89999f6b..00000000 --- a/wsnet/proto_test.go +++ /dev/null @@ -1,235 +0,0 @@ -package wsnet - -import ( - "fmt" - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" -) - -func Test_BrokerMessage(t *testing.T) { - t.Run("getAddress", func(t *testing.T) { - t.Run("OK", func(t *testing.T) { - var ( - msg = BrokerMessage{ - Policies: nil, - } - network = "tcp" - addr = "localhost:1234" - ) - - protocol := formatAddress(network, addr) - gotNetwork, gotAddr, err := msg.getAddress(protocol) - assert.Success(t, "got address", err) - assert.Equal(t, "networks equal", network, gotNetwork) - assert.Equal(t, "addresses equal", addr, gotAddr) - - msg.Policies = []DialPolicy{} - gotNetwork, gotAddr, err = msg.getAddress(protocol) - assert.Success(t, "got address", err) - assert.Equal(t, "networks equal", network, gotNetwork) - assert.Equal(t, "addresses equal", addr, gotAddr) - }) - - t.Run("InvalidProtocol", func(t *testing.T) { - cases := []struct { - protocol string - errContains string - }{ - { - protocol: "", - errContains: "invalid", - }, - { - protocol: "a:b", - errContains: "invalid", - }, - { - protocol: "a:b:c:d", - errContains: "invalid", - }, - { - protocol: ":localhost:1234", - errContains: "network", - }, - { - protocol: "tcp::1234", - errContains: "host", - }, - { - protocol: "tcp:localhost:", - errContains: "port", - }, - { - protocol: "tcp:localhost:asdf", - errContains: "port", - }, - { - protocol: "tcp:localhost:-1", - errContains: "port", - }, - { - // Overflow uint16. - protocol: fmt.Sprintf("tcp:localhost:%v", uint(1)<<16), - errContains: "port", - }, - } - - var msg BrokerMessage - for i, c := range cases { - amsg := fmt.Sprintf("case %v %q: ", i, c) - gotNetwork, gotAddr, err := msg.getAddress(c.protocol) - assert.Error(t, amsg+"successfully got invalid address", err) - assert.ErrorContains(t, fmt.Sprintf("%verr contains %q", amsg, c.errContains), err, c.errContains) - assert.Equal(t, amsg+"empty network", "", gotNetwork) - assert.Equal(t, amsg+"empty address", "", gotAddr) - } - }) - - t.Run("ChecksPolicies", func(t *testing.T) { - // ok == true tests automatically have a bunch of non-matching dial - // policies injected in front of them. - cases := []struct { - network string - host string - port uint16 - policy DialPolicy - ok bool - }{ - { - network: "tcp", - host: "localhost", - port: 1234, - policy: dialPolicy("tcp", "localhost", 1234), - ok: true, - }, - { - network: "tcp", - host: "localhost", - port: 1234, - policy: dialPolicy("udp", "example.com", 51), - ok: false, - }, - // Network checks. - { - network: "tcp", - host: "localhost", - port: 1234, - policy: dialPolicy("", "localhost", 1234), - ok: true, - }, - { - network: "tcp", - host: "localhost", - port: 1234, - policy: dialPolicy("udp", "localhost", 1234), - ok: false, - }, - // Host checks. - { - network: "tcp", - host: "localhost", - port: 1234, - policy: dialPolicy("tcp", "", 1234), - ok: true, - }, - { - network: "tcp", - host: "localhost", - port: 1234, - policy: dialPolicy("tcp", "127.0.0.1", 1234), - ok: true, - }, - { - network: "tcp", - host: "127.0.0.1", - port: 1234, - policy: dialPolicy("tcp", "127.1.2.3", 1234), - ok: true, - }, - { - network: "tcp", - host: "[::1]", - port: 1234, - policy: dialPolicy("tcp", "127.1.2.3", 1234), - ok: true, - }, - { - network: "tcp", - host: "localhost", - port: 1234, - policy: dialPolicy("tcp", "example.com", 1234), - ok: false, - }, - { - network: "tcp", - host: "example.com", - port: 1234, - policy: dialPolicy("tcp", "localhost", 1234), - ok: false, - }, - // Port checks. - { - network: "tcp", - host: "localhost", - port: 1234, - policy: dialPolicy("tcp", "localhost", 5678), - ok: false, - }, - { - network: "tcp", - host: "localhost", - port: 1234, - policy: dialPolicy("tcp", "localhost", 0), - ok: true, - }, - } - - for i, c := range cases { - var ( - amsg = fmt.Sprintf("case %v '%+v': ", i, c) - msg = BrokerMessage{ - Policies: []DialPolicy{c.policy}, - } - ) - - // Add nonsense policies before the matching policy. - if c.ok { - msg.Policies = []DialPolicy{ - dialPolicy("asdf", "localhost", 1234), - dialPolicy("tcp", "asdf", 1234), - dialPolicy("tcp", "localhost", 17208), - c.policy, - } - } - - // Test DialPolicy. - assert.Equal(t, amsg+"policy matches", c.ok, c.policy.permits(c.network, canonicalizeHost(c.host), c.port)) - - // Test BrokerMessage. - protocol := formatAddress(c.network, fmt.Sprintf("%v:%v", c.host, c.port)) - gotNetwork, gotAddr, err := msg.getAddress(protocol) - if c.ok { - assert.Success(t, amsg, err) - } else { - assert.Error(t, amsg+"successfully got invalid address", err) - assert.ErrorContains(t, amsg+"err contains 'not permitted'", err, "not permitted") - assert.Equal(t, amsg+"empty network", "", gotNetwork) - assert.Equal(t, amsg+"empty address", "", gotAddr) - } - } - }) - }) -} - -func formatAddress(network, addr string) string { - return fmt.Sprintf("%v:%v", network, addr) -} - -func dialPolicy(network, host string, port uint16) DialPolicy { - return DialPolicy{ - Network: network, - Host: host, - Port: port, - } -} diff --git a/wsnet/rtc.go b/wsnet/rtc.go deleted file mode 100644 index 32a089a2..00000000 --- a/wsnet/rtc.go +++ /dev/null @@ -1,290 +0,0 @@ -package wsnet - -import ( - "context" - "crypto/tls" - "encoding/json" - "errors" - "fmt" - "io" - "net" - "strings" - "sync" - "time" - - "github.com/pion/dtls/v2" - "github.com/pion/ice/v2" - "github.com/pion/logging" - "github.com/pion/turn/v2" - "github.com/pion/webrtc/v3" - "golang.org/x/net/proxy" -) - -var ( - // ErrMismatchedProtocol occurs when a TURN is requested to a STUN server, - // or a TURN server is requested instead of TURNS. - ErrMismatchedProtocol = errors.New("mismatched protocols") - // ErrInvalidCredentials occurs when invalid credentials are passed to a - // TURN server. This error cannot occur for STUN servers, as they don't accept - // credentials. - ErrInvalidCredentials = errors.New("invalid credentials") - - // Constant for the control channel protocol. - controlChannel = "control" -) - -// DialICEOptions provides options for dialing an ICE server. -type DialICEOptions struct { - Timeout time.Duration - // Whether to ignore TLS errors. - InsecureSkipVerify bool -} - -// DialICE confirms ICE servers are dialable. -// Timeout defaults to 200ms. -func DialICE(server webrtc.ICEServer, options *DialICEOptions) error { - if options == nil { - options = &DialICEOptions{} - } - - for _, rawURL := range server.URLs { - err := dialICEURL(server, rawURL, options) - if err != nil { - return err - } - } - return nil -} - -func dialICEURL(server webrtc.ICEServer, rawURL string, options *DialICEOptions) error { - url, err := ice.ParseURL(rawURL) - if err != nil { - return err - } - var ( - tcpConn net.Conn - udpConn net.PacketConn - turnServerAddr = fmt.Sprintf("%s:%d", url.Host, url.Port) - ) - switch { - case url.Scheme == ice.SchemeTypeTURN || url.Scheme == ice.SchemeTypeSTUN: - switch url.Proto { - case ice.ProtoTypeUDP: - udpConn, err = net.ListenPacket("udp4", "0.0.0.0:0") - case ice.ProtoTypeTCP: - tcpConn, err = net.Dial("tcp4", turnServerAddr) - } - case url.Scheme == ice.SchemeTypeTURNS || url.Scheme == ice.SchemeTypeSTUNS: - switch url.Proto { - case ice.ProtoTypeUDP: - udpAddr, resErr := net.ResolveUDPAddr("udp4", turnServerAddr) - if resErr != nil { - return resErr - } - dconn, dialErr := dtls.Dial("udp4", udpAddr, &dtls.Config{ - InsecureSkipVerify: options.InsecureSkipVerify, - }) - err = dialErr - udpConn = turn.NewSTUNConn(dconn) - case ice.ProtoTypeTCP: - tcpConn, err = tls.Dial("tcp4", turnServerAddr, &tls.Config{ - InsecureSkipVerify: options.InsecureSkipVerify, - }) - } - } - - if err != nil { - return err - } - if tcpConn != nil { - udpConn = turn.NewSTUNConn(tcpConn) - } - defer udpConn.Close() - - var pass string - if server.Credential != nil && server.CredentialType == webrtc.ICECredentialTypePassword { - pass = server.Credential.(string) - } - - client, err := turn.NewClient(&turn.ClientConfig{ - STUNServerAddr: turnServerAddr, - TURNServerAddr: turnServerAddr, - Username: server.Username, - Password: pass, - Realm: "", - Conn: udpConn, - RTO: options.Timeout, - }) - if err != nil { - return err - } - defer client.Close() - err = client.Listen() - if err != nil { - return err - } - // STUN servers are not authenticated with credentials. - // As long as the transport is valid, this should always work. - _, err = client.SendBindingRequest() - if err != nil { - // Transport failed to connect. - // https://github.com/pion/turn/blob/8231b69046f562420299916e9fb69cbff4754231/errors.go#L20 - if strings.Contains(err.Error(), "retransmissions failed") { - return ErrMismatchedProtocol - } - return fmt.Errorf("binding: %w", err) - } - if url.Scheme == ice.SchemeTypeTURN || url.Scheme == ice.SchemeTypeTURNS { - // We TURN to validate server credentials are correct. - pc, err := client.Allocate() - if err != nil { - if strings.Contains(err.Error(), "error 400") { - return ErrInvalidCredentials - } - // Since TURN and STUN follow the same protocol, they can - // both handshake, but once a tunnel is allocated it will - // fail to transmit. - if strings.Contains(err.Error(), "retransmissions failed") { - return ErrMismatchedProtocol - } - return err - } - defer pc.Close() - } - return nil -} - -// Generalizes creating a new peer connection with consistent options. -func newPeerConnection(servers []webrtc.ICEServer, dialer proxy.Dialer) (*webrtc.PeerConnection, error) { - se := webrtc.SettingEngine{} - se.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeUDP4}) - se.SetSrflxAcceptanceMinWait(0) - se.DetachDataChannels() - // If the disconnect and keep-alive timeouts are too closely related, we'll - // experience "random" connection failures. - se.SetICETimeouts(time.Second*5, time.Second*25, time.Second*2) - lf := logging.NewDefaultLoggerFactory() - lf.DefaultLogLevel = logging.LogLevelDisabled - se.LoggerFactory = lf - - // Enables tunneling of TURN traffic through an arbitrary proxy. - // We proxy TURN over a WebSocket to reduce deployment complexity. - if dialer != nil { - se.SetICEProxyDialer(dialer) - } - - transportPolicy := webrtc.ICETransportPolicyAll - - // If one server is provided and we know it's TURN, we can set the - // relay acceptable so the connection starts immediately. - if len(servers) == 1 { - server := servers[0] - if len(server.URLs) == 1 { - url, err := ice.ParseURL(server.URLs[0]) - if err == nil && server.Credential != nil && url.Proto == ice.ProtoTypeTCP { - se.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6}) - se.SetRelayAcceptanceMinWait(0) - } - if err == nil && (url.Scheme == ice.SchemeTypeTURN || url.Scheme == ice.SchemeTypeTURNS) { - // Local peers will connect if they discover they live on the same host. - // For testing purposes, it's simpler if they cannot peer on the same host. - transportPolicy = webrtc.ICETransportPolicyRelay - } - } - } - api := webrtc.NewAPI(webrtc.WithSettingEngine(se)) - - return api.NewPeerConnection(webrtc.Configuration{ - ICEServers: servers, - ICETransportPolicy: transportPolicy, - }) -} - -// Proxies ICE candidates using the protocol to a writer. -func proxyICECandidates(conn *webrtc.PeerConnection, w io.Writer) func() { - var ( - mut sync.Mutex - queue = []*webrtc.ICECandidate{} - flushed = false - write = func(i *webrtc.ICECandidate) { - b, _ := json.Marshal(&BrokerMessage{ - Candidate: i.ToJSON().Candidate, - }) - _, _ = w.Write(b) - } - ) - - conn.OnICECandidate(func(i *webrtc.ICECandidate) { - if i == nil { - return - } - mut.Lock() - defer mut.Unlock() - if !flushed { - queue = append(queue, i) - return - } - - write(i) - }) - return func() { - mut.Lock() - defer mut.Unlock() - for _, i := range queue { - write(i) - } - flushed = true - } -} - -// Waits for a PeerConnection to hit the open state. -func waitForConnectionOpen(ctx context.Context, conn *webrtc.PeerConnection) error { - if conn.ConnectionState() == webrtc.PeerConnectionStateConnected { - return nil - } - var cancel context.CancelFunc - if _, deadlineSet := ctx.Deadline(); deadlineSet { - ctx, cancel = context.WithCancel(ctx) - } else { - ctx, cancel = context.WithTimeout(ctx, time.Second*15) - } - defer cancel() - conn.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { - if pcs == webrtc.PeerConnectionStateConnected { - cancel() - } - }) - <-ctx.Done() - if ctx.Err() == context.DeadlineExceeded { - return context.DeadlineExceeded - } - return nil -} - -// Waits for a DataChannel to hit the open state. -func waitForDataChannelOpen(ctx context.Context, channel *webrtc.DataChannel) error { - if channel.ReadyState() == webrtc.DataChannelStateOpen { - return nil - } - if channel.ReadyState() != webrtc.DataChannelStateConnecting { - return fmt.Errorf("channel closed") - } - ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) - defer cancelFunc() - channel.OnOpen(func() { - cancelFunc() - }) - <-ctx.Done() - if ctx.Err() == context.DeadlineExceeded { - return ctx.Err() - } - return nil -} - -func stringPtr(s string) *string { - return &s -} - -func boolPtr(b bool) *bool { - return &b -} diff --git a/wsnet/rtc_test.go b/wsnet/rtc_test.go deleted file mode 100644 index 73d1af2f..00000000 --- a/wsnet/rtc_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package wsnet - -import ( - "errors" - "fmt" - "testing" - "time" - - "github.com/pion/ice/v2" - "github.com/pion/webrtc/v3" -) - -func TestDialICE(t *testing.T) { - t.Parallel() - - t.Run("TURN with TLS", func(t *testing.T) { - t.Parallel() - - addr, _ := createTURNServer(t, ice.SchemeTypeTURNS) - err := DialICE(webrtc.ICEServer{ - URLs: []string{fmt.Sprintf("turns:%s", addr)}, - Username: "example", - Credential: testPass, - CredentialType: webrtc.ICECredentialTypePassword, - }, &DialICEOptions{ - Timeout: time.Millisecond, - InsecureSkipVerify: true, - }) - if err != nil { - t.Error(err) - } - }) - - t.Run("Protocol mismatch", func(t *testing.T) { - t.Parallel() - - addr, _ := createTURNServer(t, ice.SchemeTypeTURNS) - err := DialICE(webrtc.ICEServer{ - URLs: []string{fmt.Sprintf("turn:%s", addr)}, - Username: "example", - Credential: testPass, - CredentialType: webrtc.ICECredentialTypePassword, - }, &DialICEOptions{ - Timeout: time.Millisecond, - InsecureSkipVerify: true, - }) - if !errors.Is(err, ErrMismatchedProtocol) { - t.Error(err) - } - }) - - t.Run("Invalid auth", func(t *testing.T) { - t.Parallel() - - addr, _ := createTURNServer(t, ice.SchemeTypeTURNS) - err := DialICE(webrtc.ICEServer{ - URLs: []string{fmt.Sprintf("turns:%s", addr)}, - Username: "example", - Credential: "invalid", - CredentialType: webrtc.ICECredentialTypePassword, - }, &DialICEOptions{ - Timeout: time.Millisecond, - InsecureSkipVerify: true, - }) - if !errors.Is(err, ErrInvalidCredentials) { - t.Error(err) - } - }) - - t.Run("Protocol mismatch public", func(t *testing.T) { - t.Parallel() - - err := DialICE(webrtc.ICEServer{ - URLs: []string{"turn:stun.l.google.com:19302"}, - }, &DialICEOptions{ - Timeout: time.Millisecond, - InsecureSkipVerify: true, - }) - if !errors.Is(err, ErrMismatchedProtocol) { - t.Error(err) - } - }) -} diff --git a/wsnet/wsnet_test.go b/wsnet/wsnet_test.go deleted file mode 100644 index 20aa7699..00000000 --- a/wsnet/wsnet_test.go +++ /dev/null @@ -1,195 +0,0 @@ -package wsnet - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "fmt" - "io" - "math/big" - "net" - "net/http" - "sync" - "testing" - "time" - - "cdr.dev/slog/sloggers/slogtest/assert" - "github.com/hashicorp/yamux" - "github.com/pion/ice/v2" - "github.com/pion/logging" - "github.com/pion/turn/v2" - "nhooyr.io/websocket" -) - -const ( - // Password used connecting to the test TURN server. - testPass = "test" -) - -// createDumbBroker proxies sockets between /listen and /connect -// to emulate an authenticated WebSocket pair. -func createDumbBroker(t testing.TB) (connectAddr string, listenAddr string) { - listener, err := net.Listen("tcp4", "127.0.0.1:0") - if err != nil { - t.Error(err) - } - t.Cleanup(func() { - listener.Close() - }) - var ( - mux = http.NewServeMux() - sess *yamux.Session - mut sync.Mutex - ) - mux.HandleFunc("/listen", func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, nil) - if err != nil { - t.Error(err) - } - nc := websocket.NetConn(context.Background(), c, websocket.MessageBinary) - mut.Lock() - defer mut.Unlock() - sess, err = yamux.Client(nc, nil) - if err != nil { - t.Error(err) - } - }) - mux.HandleFunc("/connect", func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, nil) - if err != nil { - t.Error(err) - return - } - nc := websocket.NetConn(context.Background(), c, websocket.MessageBinary) - mut.Lock() - defer mut.Unlock() - if sess == nil { - // We discard inbound to emulate a pubsub where we don't know if anyone - // is listening on the other side. - _, _ = io.Copy(io.Discard, nc) - return - } - oc, err := sess.Open() - if err != nil { - t.Error(err) - } - go func() { - _, _ = io.Copy(nc, oc) - }() - _, _ = io.Copy(oc, nc) - }) - - s := http.Server{ - Handler: mux, - } - go func() { - _ = s.Serve(listener) - }() - return fmt.Sprintf("ws://%s/connect", listener.Addr()), fmt.Sprintf("ws://%s/listen", listener.Addr()) -} - -// createTURNServer allocates a TURN server and returns the address. -func createTURNServer(t *testing.T, server ice.SchemeType) (string, func()) { - var ( - listeners []turn.ListenerConfig - pcListeners []turn.PacketConnConfig - relay = &turn.RelayAddressGeneratorStatic{ - RelayAddress: net.ParseIP("127.0.0.1"), - Address: "127.0.0.1", - } - listenAddr net.Addr - ) - url, _ := ice.ParseURL(fmt.Sprintf("%s:localhost", server)) - - switch url.Proto { - case ice.ProtoTypeTCP: - var ( - tcpListener net.Listener - err error - ) - if url.IsSecure() { - tcpListener, err = tls.Listen("tcp4", "127.0.0.1:0", generateTLSConfig(t)) - } else { - tcpListener, err = net.Listen("tcp4", "127.0.0.1:0") - } - if err != nil { - t.Error(err) - } - listenAddr = tcpListener.Addr() - listeners = []turn.ListenerConfig{{ - Listener: tcpListener, - RelayAddressGenerator: relay, - }} - case ice.ProtoTypeUDP: - udpListener, err := net.ListenPacket("udp4", "127.0.0.1:0") - if err != nil { - t.Error(err) - } - listenAddr = udpListener.LocalAddr() - pcListeners = []turn.PacketConnConfig{{ - PacketConn: udpListener, - RelayAddressGenerator: relay, - }} - } - - lf := logging.NewDefaultLoggerFactory() - lf.DefaultLogLevel = logging.LogLevelDisabled - srv, err := turn.NewServer(turn.ServerConfig{ - PacketConnConfigs: pcListeners, - ListenerConfigs: listeners, - Realm: "coder", - AuthHandler: func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) { - return turn.GenerateAuthKey(username, realm, testPass), true - }, - LoggerFactory: lf, - }) - if err != nil { - t.Error(err) - } - closeFunc := func() { - for _, l := range listeners { - l.Listener.Close() - } - for _, l := range pcListeners { - l.PacketConn.Close() - } - srv.Close() - } - t.Cleanup(closeFunc) - - return listenAddr.String(), closeFunc -} - -func generateTLSConfig(t testing.TB) *tls.Config { - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - assert.Success(t, "generate key", err) - template := x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - Organization: []string{"Acme Co"}, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour * 24 * 180), - - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, - } - derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) - assert.Success(t, "create certificate", err) - certBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) - privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) - assert.Success(t, "marshal private key", err) - keyBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyBytes}) - cert, err := tls.X509KeyPair(certBytes, keyBytes) - assert.Success(t, "convert to key pair", err) - return &tls.Config{ - Certificates: []tls.Certificate{cert}, - InsecureSkipVerify: true, - } -}