Merge branch 'main' into create-user/presleyp/734

This commit is contained in:
Presley
2022-04-25 16:57:17 +00:00
81 changed files with 2549 additions and 802 deletions
+3 -3
View File
@@ -94,7 +94,7 @@ jobs:
- name: Install Protoc
uses: arduino/setup-protoc@v1
with:
version: "3.19.4"
version: "3.20.0"
- uses: actions/setup-go@v3
with:
go-version: "~1.18"
@@ -172,7 +172,7 @@ jobs:
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
- name: Install goreleaser
uses: jaxxstorm/action-install-gh-release@v1.4.0
uses: jaxxstorm/action-install-gh-release@v1.5.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -241,7 +241,7 @@ jobs:
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
- name: Install goreleaser
uses: jaxxstorm/action-install-gh-release@v1.4.0
uses: jaxxstorm/action-install-gh-release@v1.5.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
+2
View File
@@ -35,3 +35,5 @@ site/out/
*.tfplan
*.lock.hcl
.terraform/
.vscode/*.log
+2
View File
@@ -35,6 +35,7 @@
"nolint",
"nosec",
"ntqry",
"OIDC",
"oneof",
"parameterscopeid",
"pqtype",
@@ -46,6 +47,7 @@
"ptytest",
"retrier",
"sdkproto",
"Signup",
"stretchr",
"TCGETS",
"tcpip",
+3 -15
View File
@@ -15,7 +15,7 @@ coderd/database/dump.sql: $(wildcard coderd/database/migrations/*.sql)
.PHONY: coderd/database/dump.sql
# Generates Go code for querying the database.
coderd/database/generate: fmt/sql coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql)
coderd/database/generate: coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql)
coderd/database/generate.sh
.PHONY: coderd/database/generate
@@ -34,22 +34,10 @@ else
endif
.PHONY: fmt/prettier
fmt/sql: $(wildcard coderd/database/queries/*.sql)
for fi in coderd/database/queries/*.sql; do \
npx sql-formatter \
--language postgresql \
--lines-between-queries 2 \
--tab-indent \
$$fi \
--output $$fi; \
done
sed -i 's/@ /@/g' ./coderd/database/queries/*.sql
fmt/terraform: $(wildcard *.tf)
terraform fmt -recursive
fmt: fmt/prettier fmt/sql fmt/terraform
fmt: fmt/prettier fmt/terraform
.PHONY: fmt
gen: coderd/database/generate peerbroker/proto provisionersdk/proto provisionerd/proto apitypings/generate
@@ -57,7 +45,7 @@ gen: coderd/database/generate peerbroker/proto provisionersdk/proto provisionerd
install: bin
@echo "--- Copying from bin to $(INSTALL_DIR)"
cp -r ./dist/coder_$(GOOS)_$(GOARCH)/* $(INSTALL_DIR)
cp -r ./dist/coder-$(GOOS)_$(GOOS)_$(GOARCH)*/* $(INSTALL_DIR)
@echo "-- CLI available at $(shell ls $(INSTALL_DIR)/coder*)"
.PHONY: install
+4
View File
@@ -120,6 +120,10 @@ func workspaceAgent() *cobra.Command {
if err != nil {
return xerrors.Errorf("writing agent session token to config: %w", err)
}
err = cfg.URL().Write(client.URL.String())
if err != nil {
return xerrors.Errorf("writing agent url to config: %w", err)
}
closer := agent.New(client.ListenWorkspaceAgent, logger)
<-cmd.Context().Done()
+9
View File
@@ -14,6 +14,7 @@ import (
"fmt"
"os"
"strconv"
"strings"
"github.com/spf13/pflag"
)
@@ -27,6 +28,14 @@ func StringVarP(flagset *pflag.FlagSet, p *string, name string, shorthand string
flagset.StringVarP(p, name, shorthand, v, fmtUsage(usage, env))
}
func StringArrayVarP(flagset *pflag.FlagSet, ptr *[]string, name string, shorthand string, env string, def []string, usage string) {
val, ok := os.LookupEnv(env)
if ok {
def = strings.Split(val, ",")
}
flagset.StringArrayVarP(ptr, name, shorthand, def, usage)
}
// Uint8VarP sets a uint8 flag on the given flag set.
func Uint8VarP(flagset *pflag.FlagSet, ptr *uint8, name string, shorthand string, env string, def uint8, usage string) {
val, ok := os.LookupEnv(env)
+20
View File
@@ -54,6 +54,26 @@ func TestCliflag(t *testing.T) {
require.NotContains(t, flagset.FlagUsages(), " - consumes")
})
t.Run("StringArrayDefault", func(t *testing.T) {
var ptr []string
flagset, name, shorthand, env, usage := randomFlag()
def := []string{"hello"}
cliflag.StringArrayVarP(flagset, &ptr, name, shorthand, env, def, usage)
got, err := flagset.GetStringArray(name)
require.NoError(t, err)
require.Equal(t, def, got)
})
t.Run("StringArrayEnvVar", func(t *testing.T) {
var ptr []string
flagset, name, shorthand, env, usage := randomFlag()
t.Setenv(env, "wow,test")
cliflag.StringArrayVarP(flagset, &ptr, name, shorthand, env, nil, usage)
got, err := flagset.GetStringArray(name)
require.NoError(t, err)
require.Equal(t, []string{"wow", "test"}, got)
})
t.Run("IntDefault", func(t *testing.T) {
var ptr uint8
flagset, name, shorthand, env, usage := randomFlag()
+29 -6
View File
@@ -1,9 +1,14 @@
package cli
import (
"fmt"
"net/url"
"os"
"os/exec"
"strings"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
)
@@ -14,15 +19,20 @@ func gitssh() *cobra.Command {
Hidden: true,
Short: `Wraps the "ssh" command and uses the coder gitssh key for authentication`,
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return xerrors.Errorf("create codersdk client: %w", err)
}
cfg := createConfig(cmd)
rawURL, err := cfg.URL().Read()
if err != nil {
return xerrors.Errorf("read agent url from config: %w", err)
}
parsedURL, err := url.Parse(rawURL)
if err != nil {
return xerrors.Errorf("parse agent url from config: %w", err)
}
session, err := cfg.AgentSession().Read()
if err != nil {
return xerrors.Errorf("read agent session from config: %w", err)
}
client := codersdk.New(parsedURL)
client.SessionToken = session
key, err := client.AgentGitSSHKey(cmd.Context())
@@ -47,12 +57,25 @@ func gitssh() *cobra.Command {
return xerrors.Errorf("close temp gitsshkey file: %w", err)
}
a := append([]string{"-i", privateKeyFile.Name()}, args...)
c := exec.CommandContext(cmd.Context(), "ssh", a...)
args = append([]string{"-i", privateKeyFile.Name()}, args...)
c := exec.CommandContext(cmd.Context(), "ssh", args...)
c.Stderr = cmd.ErrOrStderr()
c.Stdout = cmd.OutOrStdout()
c.Stdin = cmd.InOrStdin()
err = c.Run()
if err != nil {
exitErr := &exec.ExitError{}
if xerrors.As(err, &exitErr) && exitErr.ExitCode() == 255 {
_, _ = fmt.Fprintln(cmd.ErrOrStderr(),
"\n"+cliui.Styles.Wrap.Render("Coder authenticates with "+cliui.Styles.Field.Render("git")+
" using the public key below. All clones with SSH are authenticated automatically 🪄.")+"\n")
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), cliui.Styles.Code.Render(strings.TrimSpace(key.PublicKey))+"\n")
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Add to GitHub and GitLab:")
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), cliui.Styles.Prompt.String()+"https://github.com/settings/ssh/new")
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), cliui.Styles.Prompt.String()+"https://gitlab.com/-/profile/keys")
_, _ = fmt.Fprintln(cmd.ErrOrStderr())
return err
}
return xerrors.Errorf("run ssh command: %w", err)
}
+15 -2
View File
@@ -1,15 +1,19 @@
package cli
import (
"strings"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
)
func publickey() *cobra.Command {
return &cobra.Command{
Use: "publickey",
Use: "publickey",
Aliases: []string{"pubkey"},
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
@@ -21,7 +25,16 @@ func publickey() *cobra.Command {
return xerrors.Errorf("create codersdk client: %w", err)
}
cmd.Println(key.PublicKey)
cmd.Println(cliui.Styles.Wrap.Render(
"This is your public key for using " + cliui.Styles.Field.Render("git") + " in " +
"Coder. All clones with SSH will be authenticated automatically 🪄.",
))
cmd.Println()
cmd.Println(cliui.Styles.Code.Render(strings.TrimSpace(key.PublicKey)))
cmd.Println()
cmd.Println("Add to GitHub and GitLab:")
cmd.Println(cliui.Styles.Prompt.String() + "https://github.com/settings/ssh/new")
cmd.Println(cliui.Styles.Prompt.String() + "https://gitlab.com/-/profile/keys")
return nil
},
+2 -2
View File
@@ -43,7 +43,7 @@ func Root() *cobra.Command {
`,
Example: cliui.Styles.Paragraph.Render(`Start Coder in "dev" mode. This dev-mode requires no further setup, and your local `+cliui.Styles.Code.Render("coder")+` CLI will be authenticated to talk to it. This makes it easy to experiment with Coder.`) + `
` + cliui.Styles.Code.Render("$ coder start --dev") + `
` + cliui.Styles.Code.Render("$ coder server --dev") + `
` + cliui.Styles.Paragraph.Render("Get started by creating a template from an example.") + `
` + cliui.Styles.Code.Render("$ coder templates init"),
@@ -63,7 +63,7 @@ func Root() *cobra.Command {
cmd.AddCommand(
configSSH(),
start(),
server(),
login(),
parameters(),
templates(),
+86 -15
View File
@@ -18,8 +18,12 @@ import (
"github.com/briandowns/spinner"
"github.com/coreos/go-systemd/daemon"
"github.com/google/go-github/v43/github"
"github.com/pion/turn/v2"
"github.com/pion/webrtc/v3"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
xgithub "golang.org/x/oauth2/github"
"golang.org/x/xerrors"
"google.golang.org/api/idtoken"
"google.golang.org/api/option"
@@ -43,7 +47,8 @@ import (
"github.com/coder/coder/provisionersdk/proto"
)
func start() *cobra.Command {
// nolint:gocyclo
func server() *cobra.Command {
var (
accessURL string
address string
@@ -51,23 +56,28 @@ func start() *cobra.Command {
dev bool
postgresURL string
// provisionerDaemonCount is a uint8 to ensure a number > 0.
provisionerDaemonCount uint8
tlsCertFile string
tlsClientCAFile string
tlsClientAuth string
tlsEnable bool
tlsKeyFile string
tlsMinVersion string
turnRelayAddress string
skipTunnel bool
traceDatadog bool
secureAuthCookie bool
sshKeygenAlgorithmRaw string
spooky bool
provisionerDaemonCount uint8
oauth2GithubClientID string
oauth2GithubClientSecret string
oauth2GithubAllowedOrganizations []string
oauth2GithubAllowSignups bool
tlsCertFile string
tlsClientCAFile string
tlsClientAuth string
tlsEnable bool
tlsKeyFile string
tlsMinVersion string
turnRelayAddress string
skipTunnel bool
stunServers []string
traceDatadog bool
secureAuthCookie bool
sshKeygenAlgorithmRaw string
spooky bool
)
root := &cobra.Command{
Use: "start",
Use: "server",
RunE: func(cmd *cobra.Command, args []string) error {
logger := slog.Make(sloghuman.Sink(os.Stderr))
if traceDatadog {
@@ -169,8 +179,15 @@ func start() *cobra.Command {
return xerrors.Errorf("create turn server: %w", err)
}
iceServers := make([]webrtc.ICEServer, 0)
for _, stunServer := range stunServers {
iceServers = append(iceServers, webrtc.ICEServer{
URLs: []string{stunServer},
})
}
options := &coderd.Options{
AccessURL: accessURLParsed,
ICEServers: iceServers,
Logger: logger.Named("coderd"),
Database: databasefake.New(),
Pubsub: database.NewPubsubInMemory(),
@@ -180,6 +197,13 @@ func start() *cobra.Command {
TURNServer: turnServer,
}
if oauth2GithubClientSecret != "" {
options.GithubOAuth2Config, err = configureGithubOAuth2(accessURLParsed, oauth2GithubClientID, oauth2GithubClientSecret, oauth2GithubAllowSignups, oauth2GithubAllowedOrganizations)
if err != nil {
return xerrors.Errorf("configure github oauth2: %w", err)
}
}
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "access-url: %s\n", accessURL)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "provisioner-daemons: %d\n", provisionerDaemonCount)
_, _ = fmt.Fprintln(cmd.ErrOrStderr())
@@ -373,6 +397,14 @@ func start() *cobra.Command {
cliflag.BoolVarP(root.Flags(), &dev, "dev", "", "CODER_DEV_MODE", false, "Serve Coder in dev mode for tinkering")
cliflag.StringVarP(root.Flags(), &postgresURL, "postgres-url", "", "CODER_PG_CONNECTION_URL", "", "URL of a PostgreSQL database to connect to")
cliflag.Uint8VarP(root.Flags(), &provisionerDaemonCount, "provisioner-daemons", "", "CODER_PROVISIONER_DAEMONS", 1, "The amount of provisioner daemons to create on start.")
cliflag.StringVarP(root.Flags(), &oauth2GithubClientID, "oauth2-github-client-id", "", "CODER_OAUTH2_GITHUB_CLIENT_ID", "",
"Specifies a client ID to use for oauth2 with GitHub.")
cliflag.StringVarP(root.Flags(), &oauth2GithubClientSecret, "oauth2-github-client-secret", "", "CODER_OAUTH2_GITHUB_CLIENT_SECRET", "",
"Specifies a client secret to use for oauth2 with GitHub.")
cliflag.StringArrayVarP(root.Flags(), &oauth2GithubAllowedOrganizations, "oauth2-github-allowed-orgs", "", "CODER_OAUTH2_GITHUB_ALLOWED_ORGS", nil,
"Specifies organizations the user must be a member of to authenticate with GitHub.")
cliflag.BoolVarP(root.Flags(), &oauth2GithubAllowSignups, "oauth2-github-allow-signups", "", "CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS", false,
"Specifies whether new users can sign up with GitHub.")
cliflag.BoolVarP(root.Flags(), &tlsEnable, "tls-enable", "", "CODER_TLS_ENABLE", false, "Specifies if TLS will be enabled")
cliflag.StringVarP(root.Flags(), &tlsCertFile, "tls-cert-file", "", "CODER_TLS_CERT_FILE", "",
"Specifies the path to the certificate for TLS. It requires a PEM-encoded file. "+
@@ -389,6 +421,9 @@ func start() *cobra.Command {
`Specifies the minimum supported version of TLS. Accepted values are "tls10", "tls11", "tls12" or "tls13"`)
cliflag.BoolVarP(root.Flags(), &skipTunnel, "skip-tunnel", "", "CODER_DEV_SKIP_TUNNEL", false, "Skip serving dev mode through an exposed tunnel for simple setup.")
_ = root.Flags().MarkHidden("skip-tunnel")
cliflag.StringArrayVarP(root.Flags(), &stunServers, "stun-server", "", "CODER_STUN_SERVERS", []string{
"stun:stun.l.google.com:19302",
}, "Specify URLs for STUN servers to enable P2P connections.")
cliflag.BoolVarP(root.Flags(), &traceDatadog, "trace-datadog", "", "CODER_TRACE_DATADOG", false, "Send tracing data to a datadog agent")
cliflag.StringVarP(root.Flags(), &turnRelayAddress, "turn-relay-address", "", "CODER_TURN_RELAY_ADDRESS", "127.0.0.1",
"Specifies the address to bind TURN connections.")
@@ -572,6 +607,42 @@ func configureTLS(listener net.Listener, tlsMinVersion, tlsClientAuth, tlsCertFi
return tls.NewListener(listener, tlsConfig), nil
}
func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string, allowSignups bool, allowOrgs []string) (*coderd.GithubOAuth2Config, error) {
redirectURL, err := accessURL.Parse("/api/v2/users/oauth2/github/callback")
if err != nil {
return nil, xerrors.Errorf("parse github oauth callback url: %w", err)
}
return &coderd.GithubOAuth2Config{
OAuth2Config: &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
Endpoint: xgithub.Endpoint,
RedirectURL: redirectURL.String(),
Scopes: []string{
"read:user",
"read:org",
"user:email",
},
},
AllowSignups: allowSignups,
AllowOrganizations: allowOrgs,
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
user, _, err := github.NewClient(client).Users.Get(ctx, "")
return user, err
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
emails, _, err := github.NewClient(client).Users.ListEmails(ctx, &github.ListOptions{})
return emails, err
},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
memberships, _, err := github.NewClient(client).Organizations.ListOrgMemberships(ctx, &github.ListOrgMembershipsOptions{
State: "active",
})
return memberships, err
},
}, nil
}
type datadogLogger struct {
logger slog.Logger
}
+9 -9
View File
@@ -29,7 +29,7 @@ import (
// This cannot be ran in parallel because it uses a signal.
// nolint:tparallel
func TestStart(t *testing.T) {
func TestServer(t *testing.T) {
t.Run("Production", func(t *testing.T) {
t.Parallel()
if runtime.GOOS != "linux" || testing.Short() {
@@ -41,7 +41,7 @@ func TestStart(t *testing.T) {
defer closeFunc()
ctx, cancelFunc := context.WithCancel(context.Background())
done := make(chan struct{})
root, cfg := clitest.New(t, "start", "--address", ":0", "--postgres-url", connectionURL)
root, cfg := clitest.New(t, "server", "--address", ":0", "--postgres-url", connectionURL)
go func() {
defer close(done)
err = root.ExecuteContext(ctx)
@@ -72,7 +72,7 @@ func TestStart(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, cfg := clitest.New(t, "start", "--dev", "--skip-tunnel", "--address", ":0")
root, cfg := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0")
go func() {
err := root.ExecuteContext(ctx)
require.ErrorIs(t, err, context.Canceled)
@@ -97,7 +97,7 @@ func TestStart(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, _ := clitest.New(t, "start", "--dev", "--skip-tunnel", "--address", ":0",
root, _ := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0",
"--tls-enable", "--tls-min-version", "tls9")
err := root.ExecuteContext(ctx)
require.Error(t, err)
@@ -106,7 +106,7 @@ func TestStart(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, _ := clitest.New(t, "start", "--dev", "--skip-tunnel", "--address", ":0",
root, _ := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0",
"--tls-enable", "--tls-client-auth", "something")
err := root.ExecuteContext(ctx)
require.Error(t, err)
@@ -115,7 +115,7 @@ func TestStart(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, _ := clitest.New(t, "start", "--dev", "--skip-tunnel", "--address", ":0",
root, _ := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0",
"--tls-enable")
err := root.ExecuteContext(ctx)
require.Error(t, err)
@@ -126,7 +126,7 @@ func TestStart(t *testing.T) {
defer cancelFunc()
certPath, keyPath := generateTLSCertificate(t)
root, cfg := clitest.New(t, "start", "--dev", "--skip-tunnel", "--address", ":0",
root, cfg := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0",
"--tls-enable", "--tls-cert-file", certPath, "--tls-key-file", keyPath)
go func() {
err := root.ExecuteContext(ctx)
@@ -162,7 +162,7 @@ func TestStart(t *testing.T) {
}
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, cfg := clitest.New(t, "start", "--dev", "--skip-tunnel", "--address", ":0", "--provisioner-daemons", "0")
root, cfg := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0", "--provisioner-daemons", "0")
done := make(chan struct{})
go func() {
defer close(done)
@@ -204,7 +204,7 @@ func TestStart(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, _ := clitest.New(t, "start", "--dev", "--skip-tunnel", "--address", ":0", "--trace-datadog=true")
root, _ := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0", "--trace-datadog=true")
done := make(chan struct{})
go func() {
defer close(done)
+13
View File
@@ -134,6 +134,19 @@ func ssh() *cobra.Command {
defer func() {
_ = term.Restore(int(os.Stdin.Fd()), state)
}()
windowChange := listenWindowSize(cmd.Context())
go func() {
for {
select {
case <-cmd.Context().Done():
return
case <-windowChange:
}
width, height, _ := term.GetSize(int(stdoutFile.Fd()))
_ = sshSession.WindowChange(height, width)
}
}()
}
err = sshSession.RequestPty("xterm-256color", 128, 128, gossh.TerminalModes{})
+22
View File
@@ -0,0 +1,22 @@
//go:build !windows
// +build !windows
package cli
import (
"context"
"os"
"os/signal"
"golang.org/x/sys/unix"
)
func listenWindowSize(ctx context.Context) <-chan os.Signal {
windowSize := make(chan os.Signal, 1)
signal.Notify(windowSize, unix.SIGWINCH)
go func() {
<-ctx.Done()
signal.Stop(windowSize)
}()
return windowSize
}
+27
View File
@@ -0,0 +1,27 @@
//go:build windows
// +build windows
package cli
import (
"context"
"os"
"time"
)
func listenWindowSize(ctx context.Context) <-chan os.Signal {
windowSize := make(chan os.Signal, 3)
ticker := time.NewTicker(time.Second)
go func() {
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
}
windowSize <- nil
}
}()
return windowSize
}
+90
View File
@@ -0,0 +1,90 @@
package cli
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/cryptorand"
)
func userCreate() *cobra.Command {
var (
email string
username string
password string
)
cmd := &cobra.Command{
Use: "create",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
if username == "" {
username, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Username:",
})
if err != nil {
return err
}
}
if email == "" {
email, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Email:",
Validate: func(s string) error {
err := validator.New().Var(s, "email")
if err != nil {
return xerrors.New("That's not a valid email address!")
}
return err
},
})
if err != nil {
return err
}
}
if password == "" {
password, err = cryptorand.StringCharset(cryptorand.Human, 12)
if err != nil {
return err
}
}
_, err = client.CreateUser(cmd.Context(), codersdk.CreateUserRequest{
Email: email,
Username: username,
Password: password,
OrganizationID: organization.ID,
})
if err != nil {
return err
}
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), `A new user has been created!
Share the instructions below to get them started.
`+cliui.Styles.Placeholder.Render("—————————————————————————————————————————————————")+`
Download the Coder command line for your operating system:
https://github.com/coder/coder/releases
Run `+cliui.Styles.Code.Render("coder login "+client.URL.String())+` to authenticate.
Your email is: `+cliui.Styles.Field.Render(email)+`
Your password is: `+cliui.Styles.Field.Render(password)+`
Create a workspace `+cliui.Styles.Code.Render("coder workspaces create")+`!`)
return nil
},
}
cmd.Flags().StringVarP(&email, "email", "e", "", "Specifies an email address for the new user.")
cmd.Flags().StringVarP(&username, "username", "u", "", "Specifies a username for the new user.")
cmd.Flags().StringVarP(&password, "password", "p", "", "Specifies a password for the new user.")
return cmd
}
+42
View File
@@ -0,0 +1,42 @@
package cli_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/pty/ptytest"
)
func TestUserCreate(t *testing.T) {
t.Parallel()
t.Run("Prompts", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
cmd, root := clitest.New(t, "users", "create")
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
require.NoError(t, err)
}()
matches := []string{
"Username", "dean",
"Email", "dean@coder.com",
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
}
<-doneChan
})
}
+46
View File
@@ -0,0 +1,46 @@
package cli
import (
"fmt"
"sort"
"time"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"github.com/coder/coder/codersdk"
)
func userList() *cobra.Command {
return &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
users, err := client.Users(cmd.Context(), codersdk.UsersRequest{})
if err != nil {
return err
}
sort.Slice(users, func(i, j int) bool {
return users[i].Username < users[j].Username
})
tableWriter := table.NewWriter()
tableWriter.SetStyle(table.StyleLight)
tableWriter.Style().Options.SeparateColumns = false
tableWriter.AppendHeader(table.Row{"Username", "Email", "Created At"})
for _, user := range users {
tableWriter.AppendRow(table.Row{
user.Username,
user.Email,
user.CreatedAt.Format(time.Stamp),
})
}
_, err = fmt.Fprintln(cmd.OutOrStdout(), tableWriter.Render())
return err
},
}
}
+30
View File
@@ -0,0 +1,30 @@
package cli_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/pty/ptytest"
)
func TestUserList(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
cmd, root := clitest.New(t, "users", "list")
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
require.NoError(t, err)
}()
pty.ExpectMatch("coder.com")
<-doneChan
}
+1
View File
@@ -6,5 +6,6 @@ func users() *cobra.Command {
cmd := &cobra.Command{
Use: "users",
}
cmd.AddCommand(userCreate(), userList())
return cmd
}
+1 -1
View File
@@ -71,7 +71,7 @@ func workspaceList() *cobra.Command {
workspace.Outdated,
})
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), tableWriter.Render())
_, err = fmt.Fprintln(cmd.OutOrStdout(), tableWriter.Render())
return err
},
}
+24
View File
@@ -4,12 +4,16 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/coder/coder/cli"
"github.com/coder/coder/cli/cliui"
)
func main() {
dadjoke()
err := cli.Root().Execute()
if err != nil {
if errors.Is(err, cliui.Canceled) {
@@ -19,3 +23,23 @@ func main() {
os.Exit(1)
}
}
//nolint
func dadjoke() {
if os.Getenv("EEOFF") != "" || filepath.Base(os.Args[0]) != "gitpod" {
return
}
args := strings.Fields(`run -it --rm git --image=index.docker.io/bitnami/git --command --restart=Never -- git`)
args = append(args, os.Args[1:]...)
cmd := exec.Command("kubectl", args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
_ = cmd.Start()
err := cmd.Wait()
if exitErr, ok := err.(*exec.ExitError); ok {
os.Exit(exitErr.ExitCode())
}
os.Exit(0)
}
+1 -1
View File
@@ -1,4 +1,4 @@
# Run "coder start --help" for flag information.
# Run "coder server --help" for flag information.
CODER_ADDRESS=
CODER_PG_CONNECTION_URL=
CODER_TLS_CERT_FILE=
+1 -1
View File
@@ -19,7 +19,7 @@ AmbientCapabilities=CAP_IPC_LOCK
CacheDirectory=coder
CapabilityBoundingSet=CAP_SYSLOG CAP_IPC_LOCK CAP_NET_BIND_SERVICE
NoNewPrivileges=yes
ExecStart=/usr/bin/coder start
ExecStart=/usr/bin/coder server
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
+36 -18
View File
@@ -35,13 +35,18 @@ type Options struct {
Pubsub database.Pubsub
AgentConnectionUpdateFrequency time.Duration
AWSCertificates awsidentity.Certificates
AzureCertificates x509.VerifyOptions
GoogleTokenValidator *idtoken.Validator
ICEServers []webrtc.ICEServer
SecureAuthCookie bool
SSHKeygenAlgorithm gitsshkey.Algorithm
TURNServer *turnconn.Server
// APIRateLimit is the minutely throughput rate limit per user or ip.
// Setting a rate limit <0 will disable the rate limiter across the entire
// app. Specific routes may have their own limiters.
APIRateLimit int
AWSCertificates awsidentity.Certificates
AzureCertificates x509.VerifyOptions
GoogleTokenValidator *idtoken.Validator
GithubOAuth2Config *GithubOAuth2Config
ICEServers []webrtc.ICEServer
SecureAuthCookie bool
SSHKeygenAlgorithm gitsshkey.Algorithm
TURNServer *turnconn.Server
}
// New constructs the Coder API into an HTTP handler.
@@ -52,16 +57,22 @@ func New(options *Options) (http.Handler, func()) {
if options.AgentConnectionUpdateFrequency == 0 {
options.AgentConnectionUpdateFrequency = 3 * time.Second
}
if options.APIRateLimit == 0 {
options.APIRateLimit = 512
}
api := &api{
Options: options,
}
apiKeyMiddleware := httpmw.ExtractAPIKey(options.Database, &httpmw.OAuth2Configs{
Github: options.GithubOAuth2Config,
})
r := chi.NewRouter()
r.Route("/api/v2", func(r chi.Router) {
r.Use(
chitrace.Middleware(),
// Specific routes can specify smaller limits.
httpmw.RateLimitPerMinute(512),
httpmw.RateLimitPerMinute(options.APIRateLimit),
debugLogRequest(api.Logger),
)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
@@ -79,7 +90,7 @@ func New(options *Options) (http.Handler, func()) {
})
r.Route("/files", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
apiKeyMiddleware,
// This number is arbitrary, but reading/writing
// file content is expensive so it should be small.
httpmw.RateLimitPerMinute(12),
@@ -89,7 +100,7 @@ func New(options *Options) (http.Handler, func()) {
})
r.Route("/organizations/{organization}", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
apiKeyMiddleware,
httpmw.ExtractOrganizationParam(options.Database),
)
r.Get("/", api.organization)
@@ -102,7 +113,7 @@ func New(options *Options) (http.Handler, func()) {
})
})
r.Route("/parameters/{scope}/{id}", func(r chi.Router) {
r.Use(httpmw.ExtractAPIKey(options.Database, nil))
r.Use(apiKeyMiddleware)
r.Post("/", api.postParameter)
r.Get("/", api.parameters)
r.Route("/{name}", func(r chi.Router) {
@@ -111,7 +122,7 @@ func New(options *Options) (http.Handler, func()) {
})
r.Route("/templates/{template}", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
apiKeyMiddleware,
httpmw.ExtractTemplateParam(options.Database),
httpmw.ExtractOrganizationParam(options.Database),
)
@@ -125,7 +136,7 @@ func New(options *Options) (http.Handler, func()) {
})
r.Route("/templateversions/{templateversion}", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
apiKeyMiddleware,
httpmw.ExtractTemplateVersionParam(options.Database),
httpmw.ExtractOrganizationParam(options.Database),
)
@@ -147,8 +158,15 @@ func New(options *Options) (http.Handler, func()) {
r.Post("/first", api.postFirstUser)
r.Post("/login", api.postLogin)
r.Post("/logout", api.postLogout)
r.Get("/authmethods", api.userAuthMethods)
r.Route("/oauth2", func(r chi.Router) {
r.Route("/github", func(r chi.Router) {
r.Use(httpmw.ExtractOAuth2(options.GithubOAuth2Config))
r.Get("/callback", api.userOAuth2Github)
})
})
r.Group(func(r chi.Router) {
r.Use(httpmw.ExtractAPIKey(options.Database, nil))
r.Use(apiKeyMiddleware)
r.Post("/", api.postUsers)
r.Get("/", api.users)
r.Route("/{user}", func(r chi.Router) {
@@ -186,7 +204,7 @@ func New(options *Options) (http.Handler, func()) {
})
r.Route("/{workspaceagent}", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
apiKeyMiddleware,
httpmw.ExtractWorkspaceAgentParam(options.Database),
)
r.Get("/", api.workspaceAgent)
@@ -197,7 +215,7 @@ func New(options *Options) (http.Handler, func()) {
})
r.Route("/workspaceresources/{workspaceresource}", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
apiKeyMiddleware,
httpmw.ExtractWorkspaceResourceParam(options.Database),
httpmw.ExtractWorkspaceParam(options.Database),
)
@@ -205,7 +223,7 @@ func New(options *Options) (http.Handler, func()) {
})
r.Route("/workspaces/{workspace}", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
apiKeyMiddleware,
httpmw.ExtractWorkspaceParam(options.Database),
)
r.Get("/", api.workspace)
@@ -223,7 +241,7 @@ func New(options *Options) (http.Handler, func()) {
})
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
apiKeyMiddleware,
httpmw.ExtractWorkspaceBuildParam(options.Database),
httpmw.ExtractWorkspaceParam(options.Database),
)
+4
View File
@@ -53,8 +53,10 @@ import (
type Options struct {
AWSCertificates awsidentity.Certificates
AzureCertificates x509.VerifyOptions
GithubOAuth2Config *coderd.GithubOAuth2Config
GoogleTokenValidator *idtoken.Validator
SSHKeygenAlgorithm gitsshkey.Algorithm
APIRateLimit int
}
// New constructs an in-memory coderd instance and returns
@@ -122,9 +124,11 @@ func New(t *testing.T, options *Options) *codersdk.Client {
AWSCertificates: options.AWSCertificates,
AzureCertificates: options.AzureCertificates,
GithubOAuth2Config: options.GithubOAuth2Config,
GoogleTokenValidator: options.GoogleTokenValidator,
SSHKeygenAlgorithm: options.SSHKeygenAlgorithm,
TURNServer: turnServer,
APIRateLimit: options.APIRateLimit,
})
t.Cleanup(func() {
cancelFunc()
+87 -23
View File
@@ -3,6 +3,7 @@ package databasefake
import (
"context"
"database/sql"
"sort"
"strings"
"sync"
@@ -164,11 +165,70 @@ func (q *fakeQuerier) GetUserCount(_ context.Context) (int64, error) {
return int64(len(q.users)), nil
}
func (q *fakeQuerier) GetUsers(_ context.Context) ([]database.User, error) {
func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams) ([]database.User, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
return q.users, nil
users := q.users
// Database orders by created_at
sort.Slice(users, func(i, j int) bool {
if users[i].CreatedAt.Equal(users[j].CreatedAt) {
// Technically the postgres database also orders by uuid. So match
// that behavior
return users[i].ID.String() < users[j].ID.String()
}
return users[i].CreatedAt.Before(users[j].CreatedAt)
})
if params.AfterUser != uuid.Nil {
found := false
for i := range users {
if users[i].ID == params.AfterUser {
// We want to return all users after index i.
if i+1 >= len(users) {
return []database.User{}, nil
}
users = users[i+1:]
found = true
break
}
}
// If no users after the time, then we return an empty list.
if !found {
return []database.User{}, nil
}
}
if params.Search != "" {
tmp := make([]database.User, 0, len(users))
for i, user := range users {
if strings.Contains(user.Email, params.Search) {
tmp = append(tmp, users[i])
} else if strings.Contains(user.Username, params.Search) {
tmp = append(tmp, users[i])
}
}
users = tmp
}
if params.OffsetOpt > 0 {
if int(params.OffsetOpt) > len(users)-1 {
return []database.User{}, nil
}
users = users[params.OffsetOpt:]
}
if params.LimitOpt > 0 {
if int(params.LimitOpt) > len(users) {
params.LimitOpt = int32(len(users))
}
users = users[:params.LimitOpt]
}
tmp := make([]database.User, len(users))
copy(tmp, users)
return tmp, nil
}
func (q *fakeQuerier) GetWorkspacesByTemplateID(_ context.Context, arg database.GetWorkspacesByTemplateIDParams) ([]database.Workspace, error) {
@@ -372,6 +432,16 @@ func (q *fakeQuerier) GetWorkspacesByUserID(_ context.Context, req database.GetW
return workspaces, nil
}
func (q *fakeQuerier) GetOrganizations(_ context.Context) ([]database.Organization, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
if len(q.organizations) == 0 {
return nil, sql.ErrNoRows
}
return q.organizations, nil
}
func (q *fakeQuerier) GetOrganizationByID(_ context.Context, id uuid.UUID) (database.Organization, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@@ -794,21 +864,18 @@ func (q *fakeQuerier) InsertAPIKey(_ context.Context, arg database.InsertAPIKeyP
//nolint:gosimple
key := database.APIKey{
ID: arg.ID,
HashedSecret: arg.HashedSecret,
UserID: arg.UserID,
Application: arg.Application,
Name: arg.Name,
LastUsed: arg.LastUsed,
ExpiresAt: arg.ExpiresAt,
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
LoginType: arg.LoginType,
OIDCAccessToken: arg.OIDCAccessToken,
OIDCRefreshToken: arg.OIDCRefreshToken,
OIDCIDToken: arg.OIDCIDToken,
OIDCExpiry: arg.OIDCExpiry,
DevurlToken: arg.DevurlToken,
ID: arg.ID,
HashedSecret: arg.HashedSecret,
UserID: arg.UserID,
ExpiresAt: arg.ExpiresAt,
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
LastUsed: arg.LastUsed,
LoginType: arg.LoginType,
OAuthAccessToken: arg.OAuthAccessToken,
OAuthRefreshToken: arg.OAuthRefreshToken,
OAuthIDToken: arg.OAuthIDToken,
OAuthExpiry: arg.OAuthExpiry,
}
q.apiKeys = append(q.apiKeys, key)
return key, nil
@@ -1047,8 +1114,6 @@ func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
user := database.User{
ID: arg.ID,
Email: arg.Email,
Name: arg.Name,
LoginType: arg.LoginType,
HashedPassword: arg.HashedPassword,
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
@@ -1066,7 +1131,6 @@ func (q *fakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUs
if user.ID != arg.ID {
continue
}
user.Name = arg.Name
user.Email = arg.Email
user.Username = arg.Username
q.users[index] = user
@@ -1123,9 +1187,9 @@ func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPI
}
apiKey.LastUsed = arg.LastUsed
apiKey.ExpiresAt = arg.ExpiresAt
apiKey.OIDCAccessToken = arg.OIDCAccessToken
apiKey.OIDCRefreshToken = arg.OIDCRefreshToken
apiKey.OIDCExpiry = arg.OIDCExpiry
apiKey.OAuthAccessToken = arg.OAuthAccessToken
apiKey.OAuthRefreshToken = arg.OAuthRefreshToken
apiKey.OAuthExpiry = arg.OAuthExpiry
q.apiKeys[index] = apiKey
return nil
}
+9 -21
View File
@@ -14,9 +14,8 @@ CREATE TYPE log_source AS ENUM (
);
CREATE TYPE login_type AS ENUM (
'built-in',
'saml',
'oidc'
'password',
'github'
);
CREATE TYPE parameter_destination_scheme AS ENUM (
@@ -67,18 +66,15 @@ CREATE TABLE api_keys (
id text NOT NULL,
hashed_secret bytea NOT NULL,
user_id uuid NOT NULL,
application boolean NOT NULL,
name text NOT NULL,
last_used timestamp with time zone NOT NULL,
expires_at timestamp with time zone NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
login_type login_type NOT NULL,
oidc_access_token text DEFAULT ''::text NOT NULL,
oidc_refresh_token text DEFAULT ''::text NOT NULL,
oidc_id_token text DEFAULT ''::text NOT NULL,
oidc_expiry timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
devurl_token boolean DEFAULT false NOT NULL
oauth_access_token text DEFAULT ''::text NOT NULL,
oauth_refresh_token text DEFAULT ''::text NOT NULL,
oauth_id_token text DEFAULT ''::text NOT NULL,
oauth_expiry timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL
);
CREATE TABLE files (
@@ -222,13 +218,10 @@ CREATE TABLE templates (
CREATE TABLE users (
id uuid NOT NULL,
email text NOT NULL,
name text NOT NULL,
revoked boolean NOT NULL,
login_type login_type NOT NULL,
username text DEFAULT ''::text NOT NULL,
hashed_password bytea NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
username text DEFAULT ''::text NOT NULL
updated_at timestamp with time zone NOT NULL
);
CREATE TABLE workspace_agents (
@@ -345,9 +338,6 @@ ALTER TABLE ONLY templates
ALTER TABLE ONLY users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
ALTER TABLE ONLY workspace_agents
ADD CONSTRAINT workspace_agents_auth_token_key UNIQUE (auth_token);
ALTER TABLE ONLY workspace_agents
ADD CONSTRAINT workspace_agents_pkey PRIMARY KEY (id);
@@ -382,13 +372,11 @@ CREATE UNIQUE INDEX idx_users_email ON users USING btree (email);
CREATE UNIQUE INDEX idx_users_username ON users USING btree (username);
CREATE UNIQUE INDEX idx_workspaces_name_lower ON workspaces USING btree (lower((name)::text));
CREATE UNIQUE INDEX templates_organization_id_name_idx ON templates USING btree (organization_id, name) WHERE (deleted = false);
CREATE UNIQUE INDEX users_username_lower_idx ON users USING btree (lower(username));
CREATE UNIQUE INDEX workspaces_owner_id_name_idx ON workspaces USING btree (owner_id, name) WHERE (deleted = false);
CREATE UNIQUE INDEX workspaces_owner_id_lower_idx ON workspaces USING btree (owner_id, lower((name)::text)) WHERE (deleted = false);
ALTER TABLE ONLY api_keys
ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+7 -22
View File
@@ -4,26 +4,18 @@
-- All tables and types are stolen from:
-- https://github.com/coder/m/blob/47b6fc383347b9f9fab424d829c482defd3e1fe2/product/coder/pkg/database/dump.sql
--
-- Name: users; Type: TABLE; Schema: public; Owner: coder
--
CREATE TYPE login_type AS ENUM (
'built-in',
'saml',
'oidc'
'password',
'github'
);
CREATE TABLE IF NOT EXISTS users (
id uuid NOT NULL,
email text NOT NULL,
name text NOT NULL,
revoked boolean NOT NULL,
login_type login_type NOT NULL,
username text DEFAULT ''::text NOT NULL,
hashed_password bytea NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
username text DEFAULT ''::text NOT NULL,
PRIMARY KEY (id)
);
@@ -31,10 +23,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users USING btree (email);
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users USING btree (username);
CREATE UNIQUE INDEX IF NOT EXISTS users_username_lower_idx ON users USING btree (lower(username));
--
-- Name: organizations; Type: TABLE; Schema: Owner: coder
--
CREATE TABLE IF NOT EXISTS organizations (
id uuid NOT NULL,
name text NOT NULL,
@@ -68,18 +56,15 @@ CREATE TABLE IF NOT EXISTS api_keys (
id text NOT NULL,
hashed_secret bytea NOT NULL,
user_id uuid NOT NULL,
application boolean NOT NULL,
name text NOT NULL,
last_used timestamp with time zone NOT NULL,
expires_at timestamp with time zone NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
login_type login_type NOT NULL,
oidc_access_token text DEFAULT ''::text NOT NULL,
oidc_refresh_token text DEFAULT ''::text NOT NULL,
oidc_id_token text DEFAULT ''::text NOT NULL,
oidc_expiry timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
devurl_token boolean DEFAULT false NOT NULL,
oauth_access_token text DEFAULT ''::text NOT NULL,
oauth_refresh_token text DEFAULT ''::text NOT NULL,
oauth_id_token text DEFAULT ''::text NOT NULL,
oauth_expiry timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
PRIMARY KEY (id)
);
@@ -12,8 +12,7 @@ CREATE TABLE workspaces (
);
-- Enforces no active workspaces have the same name.
CREATE UNIQUE INDEX ON workspaces USING btree (owner_id, name) WHERE deleted = FALSE;
CREATE UNIQUE INDEX idx_workspaces_name_lower ON workspaces USING btree (lower(name));
CREATE UNIQUE INDEX ON workspaces USING btree (owner_id, lower(name)) WHERE deleted = FALSE;
CREATE TYPE workspace_transition AS ENUM (
'start',
@@ -80,7 +80,7 @@ CREATE TABLE workspace_agents (
last_connected_at timestamptz,
disconnected_at timestamptz,
resource_id uuid NOT NULL REFERENCES workspace_resources (id) ON DELETE CASCADE,
auth_token uuid NOT NULL UNIQUE,
auth_token uuid NOT NULL,
auth_instance_id varchar(64),
architecture varchar(64) NOT NULL,
environment_variables jsonb,
+15 -22
View File
@@ -56,9 +56,8 @@ func (e *LogSource) Scan(src interface{}) error {
type LoginType string
const (
LoginTypeBuiltIn LoginType = "built-in"
LoginTypeSaml LoginType = "saml"
LoginTypeOIDC LoginType = "oidc"
LoginTypePassword LoginType = "password"
LoginTypeGithub LoginType = "github"
)
func (e *LoginType) Scan(src interface{}) error {
@@ -230,21 +229,18 @@ func (e *WorkspaceTransition) Scan(src interface{}) error {
}
type APIKey struct {
ID string `db:"id" json:"id"`
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Application bool `db:"application" json:"application"`
Name string `db:"name" json:"name"`
LastUsed time.Time `db:"last_used" json:"last_used"`
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
LoginType LoginType `db:"login_type" json:"login_type"`
OIDCAccessToken string `db:"oidc_access_token" json:"oidc_access_token"`
OIDCRefreshToken string `db:"oidc_refresh_token" json:"oidc_refresh_token"`
OIDCIDToken string `db:"oidc_id_token" json:"oidc_id_token"`
OIDCExpiry time.Time `db:"oidc_expiry" json:"oidc_expiry"`
DevurlToken bool `db:"devurl_token" json:"devurl_token"`
ID string `db:"id" json:"id"`
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
LastUsed time.Time `db:"last_used" json:"last_used"`
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
LoginType LoginType `db:"login_type" json:"login_type"`
OAuthAccessToken string `db:"oauth_access_token" json:"oauth_access_token"`
OAuthRefreshToken string `db:"oauth_refresh_token" json:"oauth_refresh_token"`
OAuthIDToken string `db:"oauth_id_token" json:"oauth_id_token"`
OAuthExpiry time.Time `db:"oauth_expiry" json:"oauth_expiry"`
}
type File struct {
@@ -378,13 +374,10 @@ type TemplateVersion struct {
type User struct {
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Name string `db:"name" json:"name"`
Revoked bool `db:"revoked" json:"revoked"`
LoginType LoginType `db:"login_type" json:"login_type"`
Username string `db:"username" json:"username"`
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Username string `db:"username" json:"username"`
}
type Workspace struct {
+2 -1
View File
@@ -18,6 +18,7 @@ type querier interface {
GetOrganizationByID(ctx context.Context, id uuid.UUID) (Organization, error)
GetOrganizationByName(ctx context.Context, name string) (Organization, error)
GetOrganizationMemberByUserID(ctx context.Context, arg GetOrganizationMemberByUserIDParams) (OrganizationMember, error)
GetOrganizations(ctx context.Context) ([]Organization, error)
GetOrganizationsByUserID(ctx context.Context, userID uuid.UUID) ([]Organization, error)
GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error)
GetParameterValueByScopeAndName(ctx context.Context, arg GetParameterValueByScopeAndNameParams) (ParameterValue, error)
@@ -38,7 +39,7 @@ type querier interface {
GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error)
GetUserByID(ctx context.Context, id uuid.UUID) (User, error)
GetUserCount(ctx context.Context) (int64, error)
GetUsers(ctx context.Context) ([]User, error)
GetUsers(ctx context.Context, arg GetUsersParams) ([]User, error)
GetWorkspaceAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (WorkspaceAgent, error)
GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error)
GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error)
+145 -116
View File
@@ -15,7 +15,7 @@ import (
const getAPIKeyByID = `-- name: GetAPIKeyByID :one
SELECT
id, hashed_secret, user_id, application, name, last_used, expires_at, created_at, updated_at, login_type, oidc_access_token, oidc_refresh_token, oidc_id_token, oidc_expiry, devurl_token
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, oauth_access_token, oauth_refresh_token, oauth_id_token, oauth_expiry
FROM
api_keys
WHERE
@@ -31,18 +31,15 @@ func (q *sqlQuerier) GetAPIKeyByID(ctx context.Context, id string) (APIKey, erro
&i.ID,
&i.HashedSecret,
&i.UserID,
&i.Application,
&i.Name,
&i.LastUsed,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.LoginType,
&i.OIDCAccessToken,
&i.OIDCRefreshToken,
&i.OIDCIDToken,
&i.OIDCExpiry,
&i.DevurlToken,
&i.OAuthAccessToken,
&i.OAuthRefreshToken,
&i.OAuthIDToken,
&i.OAuthExpiry,
)
return i, err
}
@@ -53,55 +50,33 @@ INSERT INTO
id,
hashed_secret,
user_id,
application,
"name",
last_used,
expires_at,
created_at,
updated_at,
login_type,
oidc_access_token,
oidc_refresh_token,
oidc_id_token,
oidc_expiry,
devurl_token
oauth_access_token,
oauth_refresh_token,
oauth_id_token,
oauth_expiry
)
VALUES
(
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13,
$14,
$15
) RETURNING id, hashed_secret, user_id, application, name, last_used, expires_at, created_at, updated_at, login_type, oidc_access_token, oidc_refresh_token, oidc_id_token, oidc_expiry, devurl_token
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, oauth_access_token, oauth_refresh_token, oauth_id_token, oauth_expiry
`
type InsertAPIKeyParams struct {
ID string `db:"id" json:"id"`
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Application bool `db:"application" json:"application"`
Name string `db:"name" json:"name"`
LastUsed time.Time `db:"last_used" json:"last_used"`
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
LoginType LoginType `db:"login_type" json:"login_type"`
OIDCAccessToken string `db:"oidc_access_token" json:"oidc_access_token"`
OIDCRefreshToken string `db:"oidc_refresh_token" json:"oidc_refresh_token"`
OIDCIDToken string `db:"oidc_id_token" json:"oidc_id_token"`
OIDCExpiry time.Time `db:"oidc_expiry" json:"oidc_expiry"`
DevurlToken bool `db:"devurl_token" json:"devurl_token"`
ID string `db:"id" json:"id"`
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
LastUsed time.Time `db:"last_used" json:"last_used"`
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
LoginType LoginType `db:"login_type" json:"login_type"`
OAuthAccessToken string `db:"oauth_access_token" json:"oauth_access_token"`
OAuthRefreshToken string `db:"oauth_refresh_token" json:"oauth_refresh_token"`
OAuthIDToken string `db:"oauth_id_token" json:"oauth_id_token"`
OAuthExpiry time.Time `db:"oauth_expiry" json:"oauth_expiry"`
}
func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) {
@@ -109,36 +84,30 @@ func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (
arg.ID,
arg.HashedSecret,
arg.UserID,
arg.Application,
arg.Name,
arg.LastUsed,
arg.ExpiresAt,
arg.CreatedAt,
arg.UpdatedAt,
arg.LoginType,
arg.OIDCAccessToken,
arg.OIDCRefreshToken,
arg.OIDCIDToken,
arg.OIDCExpiry,
arg.DevurlToken,
arg.OAuthAccessToken,
arg.OAuthRefreshToken,
arg.OAuthIDToken,
arg.OAuthExpiry,
)
var i APIKey
err := row.Scan(
&i.ID,
&i.HashedSecret,
&i.UserID,
&i.Application,
&i.Name,
&i.LastUsed,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.LoginType,
&i.OIDCAccessToken,
&i.OIDCRefreshToken,
&i.OIDCIDToken,
&i.OIDCExpiry,
&i.DevurlToken,
&i.OAuthAccessToken,
&i.OAuthRefreshToken,
&i.OAuthIDToken,
&i.OAuthExpiry,
)
return i, err
}
@@ -149,20 +118,20 @@ UPDATE
SET
last_used = $2,
expires_at = $3,
oidc_access_token = $4,
oidc_refresh_token = $5,
oidc_expiry = $6
oauth_access_token = $4,
oauth_refresh_token = $5,
oauth_expiry = $6
WHERE
id = $1
`
type UpdateAPIKeyByIDParams struct {
ID string `db:"id" json:"id"`
LastUsed time.Time `db:"last_used" json:"last_used"`
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
OIDCAccessToken string `db:"oidc_access_token" json:"oidc_access_token"`
OIDCRefreshToken string `db:"oidc_refresh_token" json:"oidc_refresh_token"`
OIDCExpiry time.Time `db:"oidc_expiry" json:"oidc_expiry"`
ID string `db:"id" json:"id"`
LastUsed time.Time `db:"last_used" json:"last_used"`
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
OAuthAccessToken string `db:"oauth_access_token" json:"oauth_access_token"`
OAuthRefreshToken string `db:"oauth_refresh_token" json:"oauth_refresh_token"`
OAuthExpiry time.Time `db:"oauth_expiry" json:"oauth_expiry"`
}
func (q *sqlQuerier) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error {
@@ -170,9 +139,9 @@ func (q *sqlQuerier) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDP
arg.ID,
arg.LastUsed,
arg.ExpiresAt,
arg.OIDCAccessToken,
arg.OIDCRefreshToken,
arg.OIDCExpiry,
arg.OAuthAccessToken,
arg.OAuthRefreshToken,
arg.OAuthExpiry,
)
return err
}
@@ -453,6 +422,42 @@ func (q *sqlQuerier) GetOrganizationByName(ctx context.Context, name string) (Or
return i, err
}
const getOrganizations = `-- name: GetOrganizations :many
SELECT
id, name, description, created_at, updated_at
FROM
organizations
`
func (q *sqlQuerier) GetOrganizations(ctx context.Context) ([]Organization, error) {
rows, err := q.db.QueryContext(ctx, getOrganizations)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Organization
for rows.Next() {
var i Organization
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getOrganizationsByUserID = `-- name: GetOrganizationsByUserID :many
SELECT
id, name, description, created_at, updated_at
@@ -1777,7 +1782,7 @@ func (q *sqlQuerier) UpdateTemplateVersionByID(ctx context.Context, arg UpdateTe
const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
SELECT
id, email, name, revoked, login_type, hashed_password, created_at, updated_at, username
id, email, username, hashed_password, created_at, updated_at
FROM
users
WHERE
@@ -1798,20 +1803,17 @@ func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserBy
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.Revoked,
&i.LoginType,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Username,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT
id, email, name, revoked, login_type, hashed_password, created_at, updated_at, username
id, email, username, hashed_password, created_at, updated_at
FROM
users
WHERE
@@ -1826,13 +1828,10 @@ func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.Revoked,
&i.LoginType,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Username,
)
return i, err
}
@@ -1853,13 +1852,62 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) {
const getUsers = `-- name: GetUsers :many
SELECT
id, email, name, revoked, login_type, hashed_password, created_at, updated_at, username
id, email, username, hashed_password, created_at, updated_at
FROM
users
WHERE
CASE
-- This allows using the last element on a page as effectively a cursor.
-- This is an important option for scripts that need to paginate without
-- duplicating or missing data.
WHEN $1 :: uuid != '00000000-00000000-00000000-00000000' THEN (
-- The pagination cursor is the last user of the previous page.
-- The query is ordered by the created_at field, so select all
-- users after the cursor. We also want to include any users
-- that share the created_at (super rare).
created_at >= (
SELECT
created_at
FROM
users
WHERE
id = $1
)
-- Omit the cursor from the final.
AND id != $1
)
ELSE true
END
AND CASE
WHEN $2 :: text != '' THEN (
email LIKE concat('%', $2, '%')
OR username LIKE concat('%', $2, '%')
)
ELSE true
END
ORDER BY
-- Deterministic and consistent ordering of all users, even if they share
-- a timestamp. This is to ensure consistent pagination.
(created_at, id) ASC OFFSET $3
LIMIT
-- A null limit means "no limit", so -1 means return all
NULLIF($4 :: int, -1)
`
func (q *sqlQuerier) GetUsers(ctx context.Context) ([]User, error) {
rows, err := q.db.QueryContext(ctx, getUsers)
type GetUsersParams struct {
AfterUser uuid.UUID `db:"after_user" json:"after_user"`
Search string `db:"search" json:"search"`
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]User, error) {
rows, err := q.db.QueryContext(ctx, getUsers,
arg.AfterUser,
arg.Search,
arg.OffsetOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
@@ -1870,13 +1918,10 @@ func (q *sqlQuerier) GetUsers(ctx context.Context) ([]User, error) {
if err := rows.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.Revoked,
&i.LoginType,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Username,
); err != nil {
return nil, err
}
@@ -1896,51 +1941,41 @@ INSERT INTO
users (
id,
email,
"name",
login_type,
revoked,
username,
hashed_password,
created_at,
updated_at,
username
updated_at
)
VALUES
($1, $2, $3, $4, FALSE, $5, $6, $7, $8) RETURNING id, email, name, revoked, login_type, hashed_password, created_at, updated_at, username
($1, $2, $3, $4, $5, $6) RETURNING id, email, username, hashed_password, created_at, updated_at
`
type InsertUserParams struct {
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Name string `db:"name" json:"name"`
LoginType LoginType `db:"login_type" json:"login_type"`
Username string `db:"username" json:"username"`
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Username string `db:"username" json:"username"`
}
func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, insertUser,
arg.ID,
arg.Email,
arg.Name,
arg.LoginType,
arg.Username,
arg.HashedPassword,
arg.CreatedAt,
arg.UpdatedAt,
arg.Username,
)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.Revoked,
&i.LoginType,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Username,
)
return i, err
}
@@ -1950,17 +1985,15 @@ UPDATE
users
SET
email = $2,
"name" = $3,
username = $4,
updated_at = $5
username = $3,
updated_at = $4
WHERE
id = $1 RETURNING id, email, name, revoked, login_type, hashed_password, created_at, updated_at, username
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at
`
type UpdateUserProfileParams struct {
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Name string `db:"name" json:"name"`
Username string `db:"username" json:"username"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
@@ -1969,7 +2002,6 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil
row := q.db.QueryRowContext(ctx, updateUserProfile,
arg.ID,
arg.Email,
arg.Name,
arg.Username,
arg.UpdatedAt,
)
@@ -1977,13 +2009,10 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.Revoked,
&i.LoginType,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Username,
)
return i, err
}
+8 -27
View File
@@ -14,37 +14,18 @@ INSERT INTO
id,
hashed_secret,
user_id,
application,
"name",
last_used,
expires_at,
created_at,
updated_at,
login_type,
oidc_access_token,
oidc_refresh_token,
oidc_id_token,
oidc_expiry,
devurl_token
oauth_access_token,
oauth_refresh_token,
oauth_id_token,
oauth_expiry
)
VALUES
(
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13,
$14,
$15
) RETURNING *;
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *;
-- name: UpdateAPIKeyByID :exec
UPDATE
@@ -52,8 +33,8 @@ UPDATE
SET
last_used = $2,
expires_at = $3,
oidc_access_token = $4,
oidc_refresh_token = $5,
oidc_expiry = $6
oauth_access_token = $4,
oauth_refresh_token = $5,
oauth_expiry = $6
WHERE
id = $1;
@@ -1,3 +1,9 @@
-- name: GetOrganizations :many
SELECT
*
FROM
organizations;
-- name: GetOrganizationByID :one
SELECT
*
+43 -10
View File
@@ -30,25 +30,21 @@ INSERT INTO
users (
id,
email,
"name",
login_type,
revoked,
username,
hashed_password,
created_at,
updated_at,
username
updated_at
)
VALUES
($1, $2, $3, $4, FALSE, $5, $6, $7, $8) RETURNING *;
($1, $2, $3, $4, $5, $6) RETURNING *;
-- name: UpdateUserProfile :one
UPDATE
users
SET
email = $2,
"name" = $3,
username = $4,
updated_at = $5
username = $3,
updated_at = $4
WHERE
id = $1 RETURNING *;
@@ -56,4 +52,41 @@ WHERE
SELECT
*
FROM
users;
users
WHERE
CASE
-- This allows using the last element on a page as effectively a cursor.
-- This is an important option for scripts that need to paginate without
-- duplicating or missing data.
WHEN @after_user :: uuid != '00000000-00000000-00000000-00000000' THEN (
-- The pagination cursor is the last user of the previous page.
-- The query is ordered by the created_at field, so select all
-- users after the cursor. We also want to include any users
-- that share the created_at (super rare).
created_at >= (
SELECT
created_at
FROM
users
WHERE
id = @after_user
)
-- Omit the cursor from the final.
AND id != @after_user
)
ELSE true
END
AND CASE
WHEN @search :: text != '' THEN (
email LIKE concat('%', @search, '%')
OR username LIKE concat('%', @search, '%')
)
ELSE true
END
ORDER BY
-- Deterministic and consistent ordering of all users, even if they share
-- a timestamp. This is to ensure consistent pagination.
(created_at, id) ASC OFFSET @offset_opt
LIMIT
-- A null limit means "no limit", so -1 means return all
NULLIF(@limit_opt :: int, -1);
+4 -4
View File
@@ -21,10 +21,10 @@ overrides:
rename:
api_key: APIKey
login_type_oidc: LoginTypeOIDC
oidc_access_token: OIDCAccessToken
oidc_expiry: OIDCExpiry
oidc_id_token: OIDCIDToken
oidc_refresh_token: OIDCRefreshToken
oauth_access_token: OAuthAccessToken
oauth_expiry: OAuthExpiry
oauth_id_token: OAuthIDToken
oauth_refresh_token: OAuthRefreshToken
parameter_type_system_hcl: ParameterTypeSystemHCL
userstatus: UserStatus
gitsshkey: GitSSHKey
+1
View File
@@ -105,6 +105,7 @@ func (api *api) agentGitSSHKey(rw http.ResponseWriter, r *http.Request) {
}
httpapi.Write(rw, http.StatusOK, codersdk.AgentGitSSHKey{
PublicKey: gitSSHKey.PublicKey,
PrivateKey: gitSSHKey.PrivateKey,
})
}
+33 -24
View File
@@ -20,12 +20,6 @@ import (
// AuthCookie represents the name of the cookie the API key is stored in.
const AuthCookie = "session_token"
// OAuth2Config contains a subset of functions exposed from oauth2.Config.
// It is abstracted for simple testing.
type OAuth2Config interface {
TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource
}
type apiKeyContextKey struct{}
// APIKey returns the API key from the ExtractAPIKey handler.
@@ -37,10 +31,16 @@ func APIKey(r *http.Request) database.APIKey {
return apiKey
}
// OAuth2Configs is a collection of configurations for OAuth-based authentication.
// This should be extended to support other authentication types in the future.
type OAuth2Configs struct {
Github OAuth2Config
}
// ExtractAPIKey requires authentication using a valid API key.
// It handles extending an API key if it comes close to expiry,
// updating the last used time in the database.
func ExtractAPIKey(db database.Store, oauthConfig OAuth2Config) func(http.Handler) http.Handler {
func ExtractAPIKey(db database.Store, oauth *OAuth2Configs) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(AuthCookie)
@@ -99,14 +99,24 @@ func ExtractAPIKey(db database.Store, oauthConfig OAuth2Config) func(http.Handle
// Tracks if the API key has properties updated!
changed := false
if key.LoginType == database.LoginTypeOIDC {
// Check if the OIDC token is expired!
if key.OIDCExpiry.Before(now) && !key.OIDCExpiry.IsZero() {
if key.LoginType != database.LoginTypePassword {
// Check if the OAuth token is expired!
if key.OAuthExpiry.Before(now) && !key.OAuthExpiry.IsZero() {
var oauthConfig OAuth2Config
switch key.LoginType {
case database.LoginTypeGithub:
oauthConfig = oauth.Github
default:
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("unexpected authentication type %q", key.LoginType),
})
return
}
// If it is, let's refresh it from the provided config!
token, err := oauthConfig.TokenSource(r.Context(), &oauth2.Token{
AccessToken: key.OIDCAccessToken,
RefreshToken: key.OIDCRefreshToken,
Expiry: key.OIDCExpiry,
AccessToken: key.OAuthAccessToken,
RefreshToken: key.OAuthRefreshToken,
Expiry: key.OAuthExpiry,
}).Token()
if err != nil {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
@@ -114,9 +124,9 @@ func ExtractAPIKey(db database.Store, oauthConfig OAuth2Config) func(http.Handle
})
return
}
key.OIDCAccessToken = token.AccessToken
key.OIDCRefreshToken = token.RefreshToken
key.OIDCExpiry = token.Expiry
key.OAuthAccessToken = token.AccessToken
key.OAuthRefreshToken = token.RefreshToken
key.OAuthExpiry = token.Expiry
key.ExpiresAt = token.Expiry
changed = true
}
@@ -136,21 +146,20 @@ func ExtractAPIKey(db database.Store, oauthConfig OAuth2Config) func(http.Handle
changed = true
}
// Only update the ExpiresAt once an hour to prevent database spam.
// We extend the ExpiresAt to reduce reauthentication.
// We extend the ExpiresAt to reduce re-authentication.
apiKeyLifetime := 24 * time.Hour
if key.ExpiresAt.Sub(now) <= apiKeyLifetime-time.Hour {
key.ExpiresAt = now.Add(apiKeyLifetime)
changed = true
}
if changed {
err := db.UpdateAPIKeyByID(r.Context(), database.UpdateAPIKeyByIDParams{
ID: key.ID,
ExpiresAt: key.ExpiresAt,
LastUsed: key.LastUsed,
OIDCAccessToken: key.OIDCAccessToken,
OIDCRefreshToken: key.OIDCRefreshToken,
OIDCExpiry: key.OIDCExpiry,
ID: key.ID,
LastUsed: key.LastUsed,
ExpiresAt: key.ExpiresAt,
OAuthAccessToken: key.OAuthAccessToken,
OAuthRefreshToken: key.OAuthRefreshToken,
OAuthExpiry: key.OAuthExpiry,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
+22 -18
View File
@@ -189,7 +189,6 @@ func TestAPIKey(t *testing.T) {
sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: id,
HashedSecret: hashed[:],
LastUsed: database.Now(),
ExpiresAt: database.Now().AddDate(0, 0, 1),
})
require.NoError(t, err)
@@ -207,7 +206,6 @@ func TestAPIKey(t *testing.T) {
gotAPIKey, err := db.GetAPIKeyByID(r.Context(), id)
require.NoError(t, err)
require.Equal(t, sentAPIKey.LastUsed, gotAPIKey.LastUsed)
require.Equal(t, sentAPIKey.ExpiresAt, gotAPIKey.ExpiresAt)
})
@@ -277,7 +275,7 @@ func TestAPIKey(t *testing.T) {
require.NotEqual(t, sentAPIKey.ExpiresAt, gotAPIKey.ExpiresAt)
})
t.Run("OIDCNotExpired", func(t *testing.T) {
t.Run("OAuthNotExpired", func(t *testing.T) {
t.Parallel()
var (
db = databasefake.New()
@@ -294,7 +292,7 @@ func TestAPIKey(t *testing.T) {
sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: id,
HashedSecret: hashed[:],
LoginType: database.LoginTypeOIDC,
LoginType: database.LoginTypeGithub,
LastUsed: database.Now(),
ExpiresAt: database.Now().AddDate(0, 0, 1),
})
@@ -311,7 +309,7 @@ func TestAPIKey(t *testing.T) {
require.Equal(t, sentAPIKey.ExpiresAt, gotAPIKey.ExpiresAt)
})
t.Run("OIDCRefresh", func(t *testing.T) {
t.Run("OAuthRefresh", func(t *testing.T) {
t.Parallel()
var (
db = databasefake.New()
@@ -328,9 +326,9 @@ func TestAPIKey(t *testing.T) {
sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: id,
HashedSecret: hashed[:],
LoginType: database.LoginTypeOIDC,
LoginType: database.LoginTypeGithub,
LastUsed: database.Now(),
OIDCExpiry: database.Now().AddDate(0, 0, -1),
OAuthExpiry: database.Now().AddDate(0, 0, -1),
})
require.NoError(t, err)
token := &oauth2.Token{
@@ -338,11 +336,11 @@ func TestAPIKey(t *testing.T) {
RefreshToken: "moo",
Expiry: database.Now().AddDate(0, 0, 1),
}
httpmw.ExtractAPIKey(db, &oauth2Config{
tokenSource: &oauth2TokenSource{
token: func() (*oauth2.Token, error) {
httpmw.ExtractAPIKey(db, &httpmw.OAuth2Configs{
Github: &oauth2Config{
tokenSource: oauth2TokenSource(func() (*oauth2.Token, error) {
return token, nil
},
}),
},
})(successHandler).ServeHTTP(rw, r)
res := rw.Result()
@@ -354,22 +352,28 @@ func TestAPIKey(t *testing.T) {
require.Equal(t, sentAPIKey.LastUsed, gotAPIKey.LastUsed)
require.Equal(t, token.Expiry, gotAPIKey.ExpiresAt)
require.Equal(t, token.AccessToken, gotAPIKey.OIDCAccessToken)
require.Equal(t, token.AccessToken, gotAPIKey.OAuthAccessToken)
})
}
type oauth2Config struct {
tokenSource *oauth2TokenSource
tokenSource oauth2TokenSource
}
func (o *oauth2Config) TokenSource(_ context.Context, _ *oauth2.Token) oauth2.TokenSource {
func (o *oauth2Config) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource {
return o.tokenSource
}
type oauth2TokenSource struct {
token func() (*oauth2.Token, error)
func (*oauth2Config) AuthCodeURL(string, ...oauth2.AuthCodeOption) string {
return ""
}
func (o *oauth2TokenSource) Token() (*oauth2.Token, error) {
return o.token()
func (*oauth2Config) Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
return &oauth2.Token{}, nil
}
type oauth2TokenSource func() (*oauth2.Token, error)
func (o oauth2TokenSource) Token() (*oauth2.Token, error) {
return o()
}
+132
View File
@@ -0,0 +1,132 @@
package httpmw
import (
"context"
"fmt"
"net/http"
"golang.org/x/oauth2"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/cryptorand"
)
const (
oauth2StateCookieName = "oauth_state"
oauth2RedirectCookieName = "oauth_redirect"
)
type oauth2StateKey struct{}
type OAuth2State struct {
Token *oauth2.Token
Redirect string
}
// OAuth2Config exposes a subset of *oauth2.Config functions for easier testing.
// *oauth2.Config should be used instead of implementing this in production.
type OAuth2Config interface {
AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource
}
// OAuth2 returns the state from an oauth request.
func OAuth2(r *http.Request) OAuth2State {
oauth, ok := r.Context().Value(oauth2StateKey{}).(OAuth2State)
if !ok {
panic("developer error: oauth middleware not provided")
}
return oauth
}
// ExtractOAuth2 is a middleware for automatically redirecting to OAuth
// URLs, and handling the exchange inbound. Any route that does not have
// a "code" URL parameter will be redirected.
func ExtractOAuth2(config OAuth2Config) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if config == nil {
httpapi.Write(rw, http.StatusPreconditionRequired, httpapi.Response{
Message: fmt.Sprintf("The oauth2 method requested is not configured!"),
})
return
}
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
if code == "" {
// If the code isn't provided, we'll redirect!
state, err := cryptorand.String(32)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("generate state string: %s", err),
})
return
}
http.SetCookie(rw, &http.Cookie{
Name: oauth2StateCookieName,
Value: state,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
// Redirect must always be specified, otherwise
// an old redirect could apply!
http.SetCookie(rw, &http.Cookie{
Name: oauth2RedirectCookieName,
Value: r.URL.Query().Get("redirect"),
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
http.Redirect(rw, r, config.AuthCodeURL(state, oauth2.AccessTypeOffline), http.StatusTemporaryRedirect)
return
}
if state == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "state must be provided",
})
return
}
stateCookie, err := r.Cookie(oauth2StateCookieName)
if err != nil {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: fmt.Sprintf("%q cookie must be provided", oauth2StateCookieName),
})
return
}
if stateCookie.Value != state {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "state mismatched",
})
return
}
var redirect string
stateRedirect, err := r.Cookie(oauth2RedirectCookieName)
if err == nil {
redirect = stateRedirect.Value
}
oauthToken, err := config.Exchange(r.Context(), code)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("exchange oauth code: %s", err),
})
return
}
ctx := context.WithValue(r.Context(), oauth2StateKey{}, OAuth2State{
Token: oauthToken,
Redirect: redirect,
})
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}
+98
View File
@@ -0,0 +1,98 @@
package httpmw_test
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/coder/coder/coderd/httpmw"
)
type testOAuth2Provider struct {
}
func (*testOAuth2Provider) AuthCodeURL(state string, _ ...oauth2.AuthCodeOption) string {
return "?state=" + url.QueryEscape(state)
}
func (*testOAuth2Provider) Exchange(_ context.Context, _ string, _ ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
return &oauth2.Token{
AccessToken: "hello",
}, nil
}
func (*testOAuth2Provider) TokenSource(_ context.Context, _ *oauth2.Token) oauth2.TokenSource {
return nil
}
func TestOAuth2(t *testing.T) {
t.Parallel()
t.Run("NotSetup", func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest("GET", "/", nil)
res := httptest.NewRecorder()
httpmw.ExtractOAuth2(nil)(nil).ServeHTTP(res, req)
require.Equal(t, http.StatusPreconditionRequired, res.Result().StatusCode)
})
t.Run("RedirectWithoutCode", func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest("GET", "/?redirect="+url.QueryEscape("/dashboard"), nil)
res := httptest.NewRecorder()
httpmw.ExtractOAuth2(&testOAuth2Provider{})(nil).ServeHTTP(res, req)
location := res.Header().Get("Location")
if !assert.NotEmpty(t, location) {
return
}
require.Len(t, res.Result().Cookies(), 2)
cookie := res.Result().Cookies()[1]
require.Equal(t, "/dashboard", cookie.Value)
})
t.Run("NoState", func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest("GET", "/?code=something", nil)
res := httptest.NewRecorder()
httpmw.ExtractOAuth2(&testOAuth2Provider{})(nil).ServeHTTP(res, req)
require.Equal(t, http.StatusBadRequest, res.Result().StatusCode)
})
t.Run("NoStateCookie", func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest("GET", "/?code=something&state=test", nil)
res := httptest.NewRecorder()
httpmw.ExtractOAuth2(&testOAuth2Provider{})(nil).ServeHTTP(res, req)
require.Equal(t, http.StatusUnauthorized, res.Result().StatusCode)
})
t.Run("MismatchedState", func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest("GET", "/?code=something&state=test", nil)
req.AddCookie(&http.Cookie{
Name: "oauth_state",
Value: "mismatch",
})
res := httptest.NewRecorder()
httpmw.ExtractOAuth2(&testOAuth2Provider{})(nil).ServeHTTP(res, req)
require.Equal(t, http.StatusUnauthorized, res.Result().StatusCode)
})
t.Run("ExchangeCodeAndState", func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest("GET", "/?code=test&state=something", nil)
req.AddCookie(&http.Cookie{
Name: "oauth_state",
Value: "something",
})
req.AddCookie(&http.Cookie{
Name: "oauth_redirect",
Value: "/dashboard",
})
res := httptest.NewRecorder()
httpmw.ExtractOAuth2(&testOAuth2Provider{})(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
state := httpmw.OAuth2(r)
require.Equal(t, "/dashboard", state.Redirect)
})).ServeHTTP(res, req)
})
}
-2
View File
@@ -40,8 +40,6 @@ func TestOrganizationParam(t *testing.T) {
user, err := db.InsertUser(r.Context(), database.InsertUserParams{
ID: userID,
Email: "testaccount@coder.com",
Name: "example",
LoginType: database.LoginTypeBuiltIn,
HashedPassword: hashed[:],
Username: username,
CreatedAt: database.Now(),
+6
View File
@@ -13,6 +13,12 @@ import (
// RateLimitPerMinute returns a handler that limits requests per-minute based
// on IP, endpoint, and user ID (if available).
func RateLimitPerMinute(count int) func(http.Handler) http.Handler {
// -1 is no rate limit
if count <= 0 {
return func(handler http.Handler) http.Handler {
return handler
}
}
return httprate.Limit(
count,
1*time.Minute,
-2
View File
@@ -39,8 +39,6 @@ func TestTemplateParam(t *testing.T) {
user, err := db.InsertUser(r.Context(), database.InsertUserParams{
ID: userID,
Email: "testaccount@coder.com",
Name: "example",
LoginType: database.LoginTypeBuiltIn,
HashedPassword: hashed[:],
Username: username,
CreatedAt: database.Now(),
@@ -39,8 +39,6 @@ func TestTemplateVersionParam(t *testing.T) {
user, err := db.InsertUser(r.Context(), database.InsertUserParams{
ID: userID,
Email: "testaccount@coder.com",
Name: "example",
LoginType: database.LoginTypeBuiltIn,
HashedPassword: hashed[:],
Username: username,
CreatedAt: database.Now(),
@@ -39,8 +39,6 @@ func TestWorkspaceAgentParam(t *testing.T) {
user, err := db.InsertUser(r.Context(), database.InsertUserParams{
ID: userID,
Email: "testaccount@coder.com",
Name: "example",
LoginType: database.LoginTypeBuiltIn,
HashedPassword: hashed[:],
Username: username,
CreatedAt: database.Now(),
@@ -39,8 +39,6 @@ func TestWorkspaceBuildParam(t *testing.T) {
user, err := db.InsertUser(r.Context(), database.InsertUserParams{
ID: userID,
Email: "testaccount@coder.com",
Name: "example",
LoginType: database.LoginTypeBuiltIn,
HashedPassword: hashed[:],
Username: username,
CreatedAt: database.Now(),
-2
View File
@@ -39,8 +39,6 @@ func TestWorkspaceParam(t *testing.T) {
user, err := db.InsertUser(r.Context(), database.InsertUserParams{
ID: userID,
Email: "testaccount@coder.com",
Name: "example",
LoginType: database.LoginTypeBuiltIn,
HashedPassword: hashed[:],
Username: username,
CreatedAt: database.Now(),
+16 -1
View File
@@ -17,6 +17,7 @@ import (
"github.com/moby/moby/pkg/namesgenerator"
"github.com/tabbed/pqtype"
"golang.org/x/xerrors"
protobuf "google.golang.org/protobuf/proto"
"nhooyr.io/websocket"
"storj.io/drpc/drpcmux"
"storj.io/drpc/drpcserver"
@@ -27,6 +28,7 @@ import (
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/parameter"
"github.com/coder/coder/provisionerd/proto"
"github.com/coder/coder/provisionersdk"
sdkproto "github.com/coder/coder/provisionersdk/proto"
)
@@ -47,6 +49,8 @@ func (api *api) provisionerDaemonsListen(rw http.ResponseWriter, r *http.Request
})
return
}
// Align with the frame size of yamux.
conn.SetReadLimit(256 * 1024)
daemon, err := api.Database.InsertProvisionerDaemon(r.Context(), database.InsertProvisionerDaemonParams{
ID: uuid.New(),
@@ -82,9 +86,17 @@ func (api *api) provisionerDaemonsListen(rw http.ResponseWriter, r *http.Request
_ = conn.Close(websocket.StatusInternalError, httpapi.WebsocketCloseSprintf("drpc register provisioner daemon: %s", err))
return
}
server := drpcserver.New(mux)
server := drpcserver.NewWithOptions(mux, drpcserver.Options{
Log: func(err error) {
if xerrors.Is(err, io.EOF) {
return
}
api.Logger.Debug(r.Context(), "drpc server error", slog.Error(err))
},
})
err = server.Serve(r.Context(), session)
if err != nil {
api.Logger.Debug(r.Context(), "provisioner daemon disconnected", slog.Error(err))
_ = conn.Close(websocket.StatusInternalError, httpapi.WebsocketCloseSprintf("serve: %s", err))
return
}
@@ -253,6 +265,9 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty
default:
return nil, failJob(fmt.Sprintf("unsupported storage method: %s", job.StorageMethod))
}
if protobuf.Size(protoJob) > provisionersdk.MaxMessageSize {
return nil, failJob(fmt.Sprintf("payload was too big: %d > %d", protobuf.Size(protoJob), provisionersdk.MaxMessageSize))
}
return protoJob, err
}
+48
View File
@@ -0,0 +1,48 @@
package coderd_test
import (
"context"
"crypto/rand"
"runtime"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisionersdk"
)
func TestProvisionerDaemons(t *testing.T) {
t.Parallel()
t.Run("PayloadTooBig", func(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
// Takes too long to allocate memory on Windows!
t.Skip()
}
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
data := make([]byte, provisionersdk.MaxMessageSize)
rand.Read(data)
resp, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data)
require.NoError(t, err)
t.Log(resp.Hash)
version, err := client.CreateTemplateVersion(context.Background(), user.OrganizationID, codersdk.CreateTemplateVersionRequest{
StorageMethod: database.ProvisionerStorageMethodFile,
StorageSource: resp.Hash,
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
require.Eventually(t, func() bool {
var err error
version, err = client.TemplateVersion(context.Background(), version.ID)
require.NoError(t, err)
return version.Job.Error != ""
}, 5*time.Second, 25*time.Millisecond)
})
}
+155
View File
@@ -0,0 +1,155 @@
package coderd
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/google/go-github/v43/github"
"github.com/google/uuid"
"golang.org/x/oauth2"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/codersdk"
)
// GithubOAuth2Provider exposes required functions for the Github authentication flow.
type GithubOAuth2Config struct {
httpmw.OAuth2Config
AuthenticatedUser func(ctx context.Context, client *http.Client) (*github.User, error)
ListEmails func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error)
ListOrganizationMemberships func(ctx context.Context, client *http.Client) ([]*github.Membership, error)
AllowSignups bool
AllowOrganizations []string
}
func (api *api) userAuthMethods(rw http.ResponseWriter, _ *http.Request) {
httpapi.Write(rw, http.StatusOK, codersdk.AuthMethods{
Password: true,
Github: api.GithubOAuth2Config != nil,
})
}
func (api *api) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
state := httpmw.OAuth2(r)
oauthClient := oauth2.NewClient(r.Context(), oauth2.StaticTokenSource(state.Token))
memberships, err := api.GithubOAuth2Config.ListOrganizationMemberships(r.Context(), oauthClient)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get authenticated github user organizations: %s", err),
})
return
}
var selectedMembership *github.Membership
for _, membership := range memberships {
for _, allowed := range api.GithubOAuth2Config.AllowOrganizations {
if *membership.Organization.Login != allowed {
continue
}
selectedMembership = membership
break
}
}
if selectedMembership == nil {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: fmt.Sprintf("You aren't a member of the authorized Github organizations!"),
})
return
}
emails, err := api.GithubOAuth2Config.ListEmails(r.Context(), oauthClient)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get personal github user: %s", err),
})
return
}
var user database.User
// Search for existing users with matching and verified emails.
// If a verified GitHub email matches a Coder user, we will return.
for _, email := range emails {
if email.Verified == nil {
continue
}
user, err = api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
Email: *email.Email,
})
if errors.Is(err, sql.ErrNoRows) {
continue
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get user by email: %s", err),
})
return
}
if !*email.Verified {
httpapi.Write(rw, http.StatusForbidden, httpapi.Response{
Message: fmt.Sprintf("Verify the %q email address on Github to authenticate!", *email.Email),
})
return
}
break
}
// If the user doesn't exist, create a new one!
if user.ID == uuid.Nil {
if !api.GithubOAuth2Config.AllowSignups {
httpapi.Write(rw, http.StatusForbidden, httpapi.Response{
Message: "Signups are disabled for Github authentication!",
})
return
}
var organizationID uuid.UUID
organizations, _ := api.Database.GetOrganizations(r.Context())
if len(organizations) > 0 {
// Add the user to the first organization. Once multi-organization
// support is added, we should enable a configuration map of user
// email to organization.
organizationID = organizations[0].ID
}
ghUser, err := api.GithubOAuth2Config.AuthenticatedUser(r.Context(), oauthClient)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get authenticated github user: %s", err),
})
return
}
user, _, err = api.createUser(r.Context(), codersdk.CreateUserRequest{
Email: *ghUser.Email,
Username: *ghUser.Login,
OrganizationID: organizationID,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("create user: %s", err),
})
return
}
}
_, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
UserID: user.ID,
LoginType: database.LoginTypeGithub,
OAuthAccessToken: state.Token.AccessToken,
OAuthRefreshToken: state.Token.RefreshToken,
OAuthExpiry: state.Token.Expiry,
})
if !created {
return
}
redirect := state.Redirect
if redirect == "" {
redirect = "/"
}
http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect)
}
+205
View File
@@ -0,0 +1,205 @@
package coderd_test
import (
"context"
"net/http"
"net/url"
"testing"
"github.com/google/go-github/v43/github"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
)
type oauth2Config struct{}
func (*oauth2Config) AuthCodeURL(state string, _ ...oauth2.AuthCodeOption) string {
return "/?state=" + url.QueryEscape(state)
}
func (*oauth2Config) Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
return &oauth2.Token{
AccessToken: "token",
}, nil
}
func (*oauth2Config) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource {
return nil
}
func TestUserAuthMethods(t *testing.T) {
t.Parallel()
t.Run("Password", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
methods, err := client.AuthMethods(context.Background())
require.NoError(t, err)
require.True(t, methods.Password)
require.False(t, methods.Github)
})
t.Run("Github", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
GithubOAuth2Config: &coderd.GithubOAuth2Config{},
})
methods, err := client.AuthMethods(context.Background())
require.NoError(t, err)
require.True(t, methods.Password)
require.True(t, methods.Github)
})
}
func TestUserOAuth2Github(t *testing.T) {
t.Parallel()
t.Run("NotInAllowedOrganization", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &oauth2Config{},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{{
Organization: &github.Organization{
Login: github.String("kyle"),
},
}}, nil
},
},
})
resp := oauth2Callback(t, client)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
t.Run("UnverifiedEmail", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &oauth2Config{},
AllowOrganizations: []string{"coder"},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{{
Organization: &github.Organization{
Login: github.String("coder"),
},
}}, nil
},
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
return &github.User{}, nil
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
return []*github.UserEmail{{
Email: github.String("testuser@coder.com"),
Verified: github.Bool(false),
}}, nil
},
},
})
_ = coderdtest.CreateFirstUser(t, client)
resp := oauth2Callback(t, client)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("BlockSignups", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &oauth2Config{},
AllowOrganizations: []string{"coder"},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{{
Organization: &github.Organization{
Login: github.String("coder"),
},
}}, nil
},
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
return &github.User{}, nil
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
return []*github.UserEmail{}, nil
},
},
})
resp := oauth2Callback(t, client)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("Signup", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &oauth2Config{},
AllowOrganizations: []string{"coder"},
AllowSignups: true,
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{{
Organization: &github.Organization{
Login: github.String("coder"),
},
}}, nil
},
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
return &github.User{
Login: github.String("kyle"),
Email: github.String("kyle@coder.com"),
}, nil
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
return []*github.UserEmail{}, nil
},
},
})
resp := oauth2Callback(t, client)
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
})
t.Run("Login", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &oauth2Config{},
AllowOrganizations: []string{"coder"},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{{
Organization: &github.Organization{
Login: github.String("coder"),
},
}}, nil
},
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
return &github.User{}, nil
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
return []*github.UserEmail{{
Email: github.String("testuser@coder.com"),
Verified: github.Bool(true),
}}, nil
},
},
})
_ = coderdtest.CreateFirstUser(t, client)
resp := oauth2Callback(t, client)
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
})
}
func oauth2Callback(t *testing.T, client *codersdk.Client) *http.Response {
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
state := "somestate"
oauthURL, err := client.URL.Parse("/api/v2/users/oauth2/github/callback?code=asd&state=" + state)
require.NoError(t, err)
req, err := http.NewRequest("GET", oauthURL.String(), nil)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "oauth_state",
Value: state,
})
res, err := client.HTTPClient.Do(req)
require.NoError(t, err)
t.Cleanup(func() {
_ = res.Body.Close()
})
return res
}
+198 -195
View File
@@ -1,15 +1,18 @@
package coderd
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"golang.org/x/xerrors"
@@ -23,25 +26,6 @@ import (
"github.com/coder/coder/cryptorand"
)
// Lists all the users
func (api *api) users(rw http.ResponseWriter, r *http.Request) {
users, err := api.Database.GetUsers(r.Context())
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get users: %s", err.Error()),
})
return
}
var res []codersdk.User
for _, user := range users {
res = append(res, convertUser(user))
}
httpapi.Write(rw, http.StatusOK, res)
}
// Returns whether the initial user has been created or not.
func (api *api) firstUser(rw http.ResponseWriter, r *http.Request) {
userCount, err := api.Database.GetUserCount(r.Context())
@@ -88,66 +72,10 @@ func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) {
return
}
hashedPassword, err := userpassword.Hash(createUser.Password)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("hash password: %s", err.Error()),
})
return
}
// Create the user, organization, and membership to the user.
var user database.User
var organization database.Organization
err = api.Database.InTx(func(db database.Store) error {
user, err = api.Database.InsertUser(r.Context(), database.InsertUserParams{
ID: uuid.New(),
Email: createUser.Email,
HashedPassword: []byte(hashedPassword),
Username: createUser.Username,
LoginType: database.LoginTypeBuiltIn,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create user: %w", err)
}
privateKey, publicKey, err := gitsshkey.Generate(api.SSHKeygenAlgorithm)
if err != nil {
return xerrors.Errorf("generate user gitsshkey: %w", err)
}
_, err = db.InsertGitSSHKey(r.Context(), database.InsertGitSSHKeyParams{
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
PrivateKey: privateKey,
PublicKey: publicKey,
})
if err != nil {
return xerrors.Errorf("insert user gitsshkey: %w", err)
}
organization, err = api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{
ID: uuid.New(),
Name: createUser.OrganizationName,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create organization: %w", err)
}
_, err = api.Database.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
OrganizationID: organization.ID,
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Roles: []string{"organization-admin"},
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
}
return nil
user, organizationID, err := api.createUser(r.Context(), codersdk.CreateUserRequest{
Email: createUser.Email,
Username: createUser.Username,
Password: createUser.Password,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -158,10 +86,71 @@ func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(rw, http.StatusCreated, codersdk.CreateFirstUserResponse{
UserID: user.ID,
OrganizationID: organization.ID,
OrganizationID: organizationID,
})
}
func (api *api) users(rw http.ResponseWriter, r *http.Request) {
var (
afterArg = r.URL.Query().Get("after_user")
limitArg = r.URL.Query().Get("limit")
offsetArg = r.URL.Query().Get("offset")
searchName = r.URL.Query().Get("search")
)
// createdAfter is a user uuid.
createdAfter := uuid.Nil
if afterArg != "" {
after, err := uuid.Parse(afterArg)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("after_user must be a valid uuid: %s", err.Error()),
})
return
}
createdAfter = after
}
// Default to no limit and return all users.
pageLimit := -1
if limitArg != "" {
limit, err := strconv.Atoi(limitArg)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("limit must be an integer: %s", err.Error()),
})
return
}
pageLimit = limit
}
// The default for empty string is 0.
offset, err := strconv.ParseInt(offsetArg, 10, 64)
if offsetArg != "" && err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("offset must be an integer: %s", err.Error()),
})
return
}
users, err := api.Database.GetUsers(r.Context(), database.GetUsersParams{
AfterUser: createdAfter,
OffsetOpt: int32(offset),
LimitOpt: int32(pageLimit),
Search: searchName,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
})
return
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertUsers(users))
}
// Creates a new user.
func (api *api) postUsers(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
@@ -218,56 +207,7 @@ func (api *api) postUsers(rw http.ResponseWriter, r *http.Request) {
return
}
hashedPassword, err := userpassword.Hash(createUser.Password)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("hash password: %s", err.Error()),
})
return
}
var user database.User
err = api.Database.InTx(func(db database.Store) error {
user, err = db.InsertUser(r.Context(), database.InsertUserParams{
ID: uuid.New(),
Email: createUser.Email,
HashedPassword: []byte(hashedPassword),
Username: createUser.Username,
LoginType: database.LoginTypeBuiltIn,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create user: %w", err)
}
privateKey, publicKey, err := gitsshkey.Generate(api.SSHKeygenAlgorithm)
if err != nil {
return xerrors.Errorf("generate user gitsshkey: %w", err)
}
_, err = db.InsertGitSSHKey(r.Context(), database.InsertGitSSHKeyParams{
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
PrivateKey: privateKey,
PublicKey: publicKey,
})
if err != nil {
return xerrors.Errorf("insert user gitsshkey: %w", err)
}
_, err = db.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
OrganizationID: organization.ID,
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Roles: []string{},
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
}
return nil
})
user, _, err := api.createUser(r.Context(), createUser)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
@@ -293,11 +233,6 @@ func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) {
if !httpapi.Read(rw, r, &params) {
return
}
if params.Name == nil {
params.Name = &user.Name
}
existentUser, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
Email: params.Email,
Username: params.Username,
@@ -333,7 +268,6 @@ func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) {
updatedUserProfile, err := api.Database.UpdateUserProfile(r.Context(), database.UpdateUserProfileParams{
ID: user.ID,
Name: *params.Name,
Email: params.Email,
Username: params.Username,
UpdatedAt: database.Now(),
@@ -498,41 +432,13 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) {
return
}
keyID, keySecret, err := generateAPIKeyIDSecret()
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("generate api key parts: %s", err.Error()),
})
sessionToken, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
UserID: user.ID,
LoginType: database.LoginTypePassword,
})
if !created {
return
}
hashed := sha256.Sum256([]byte(keySecret))
_, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: keyID,
UserID: user.ID,
ExpiresAt: database.Now().Add(24 * time.Hour),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
HashedSecret: hashed[:],
LoginType: database.LoginTypeBuiltIn,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert api key: %s", err.Error()),
})
return
}
// This format is consumed by the APIKey middleware.
sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret)
http.SetCookie(rw, &http.Cookie{
Name: httpmw.AuthCookie,
Value: sessionToken,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: api.SecureAuthCookie,
})
httpapi.Write(rw, http.StatusCreated, codersdk.LoginWithPasswordResponse{
SessionToken: sessionToken,
@@ -551,35 +457,15 @@ func (api *api) postAPIKey(rw http.ResponseWriter, r *http.Request) {
return
}
keyID, keySecret, err := generateAPIKeyIDSecret()
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("generate api key parts: %s", err.Error()),
})
return
}
hashed := sha256.Sum256([]byte(keySecret))
_, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: keyID,
UserID: apiKey.UserID,
ExpiresAt: database.Now().AddDate(1, 0, 0), // Expire after 1 year (same as v1)
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
HashedSecret: hashed[:],
LoginType: database.LoginTypeBuiltIn,
sessionToken, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
UserID: user.ID,
LoginType: database.LoginTypePassword,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert api key: %s", err.Error()),
})
if !created {
return
}
// This format is consumed by the APIKey middleware.
generatedAPIKey := fmt.Sprintf("%s-%s", keyID, keySecret)
httpapi.Write(rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: generatedAPIKey})
httpapi.Write(rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: sessionToken})
}
// Clear the user's session cookie
@@ -940,12 +826,129 @@ func generateAPIKeyIDSecret() (id string, secret string, err error) {
return id, secret, nil
}
func (api *api) createAPIKey(rw http.ResponseWriter, r *http.Request, params database.InsertAPIKeyParams) (string, bool) {
keyID, keySecret, err := generateAPIKeyIDSecret()
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("generate api key parts: %s", err.Error()),
})
return "", false
}
hashed := sha256.Sum256([]byte(keySecret))
_, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: keyID,
UserID: params.UserID,
ExpiresAt: database.Now().Add(24 * time.Hour),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
HashedSecret: hashed[:],
LoginType: params.LoginType,
OAuthAccessToken: params.OAuthAccessToken,
OAuthRefreshToken: params.OAuthRefreshToken,
OAuthIDToken: params.OAuthIDToken,
OAuthExpiry: params.OAuthExpiry,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert api key: %s", err.Error()),
})
return "", false
}
// This format is consumed by the APIKey middleware.
sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret)
http.SetCookie(rw, &http.Cookie{
Name: httpmw.AuthCookie,
Value: sessionToken,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: api.SecureAuthCookie,
})
return sessionToken, true
}
func (api *api) createUser(ctx context.Context, req codersdk.CreateUserRequest) (database.User, uuid.UUID, error) {
var user database.User
return user, req.OrganizationID, api.Database.InTx(func(db database.Store) error {
// If no organization is provided, create a new one for the user.
if req.OrganizationID == uuid.Nil {
organization, err := db.InsertOrganization(ctx, database.InsertOrganizationParams{
ID: uuid.New(),
Name: req.Username,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create organization: %w", err)
}
req.OrganizationID = organization.ID
}
params := database.InsertUserParams{
ID: uuid.New(),
Email: req.Email,
Username: req.Username,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
}
// If a user signs up with OAuth, they can have no password!
if req.Password != "" {
hashedPassword, err := userpassword.Hash(req.Password)
if err != nil {
return xerrors.Errorf("hash password: %w", err)
}
params.HashedPassword = []byte(hashedPassword)
}
var err error
user, err = db.InsertUser(ctx, params)
if err != nil {
return xerrors.Errorf("create user: %w", err)
}
privateKey, publicKey, err := gitsshkey.Generate(api.SSHKeygenAlgorithm)
if err != nil {
return xerrors.Errorf("generate user gitsshkey: %w", err)
}
_, err = db.InsertGitSSHKey(ctx, database.InsertGitSSHKeyParams{
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
PrivateKey: privateKey,
PublicKey: publicKey,
})
if err != nil {
return xerrors.Errorf("insert user gitsshkey: %w", err)
}
_, err = db.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
OrganizationID: req.OrganizationID,
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Roles: []string{},
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
}
return nil
})
}
func convertUser(user database.User) codersdk.User {
return codersdk.User{
ID: user.ID,
Email: user.Email,
CreatedAt: user.CreatedAt,
Username: user.Username,
Name: user.Name,
}
}
func convertUsers(users []database.User) []codersdk.User {
converted := make([]codersdk.User, 0, len(users))
for _, u := range users {
converted = append(converted, convertUser(u))
}
return converted
}
+157 -27
View File
@@ -2,7 +2,9 @@ package coderd_test
import (
"context"
"fmt"
"net/http"
"sort"
"testing"
"github.com/google/uuid"
@@ -240,13 +242,14 @@ func TestUpdateUserProfile(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
existentUser, _ := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
existentUser, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
Email: "bruno@coder.com",
Username: "bruno",
Password: "password",
OrganizationID: user.OrganizationID,
})
_, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
require.NoError(t, err)
_, err = client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
Username: existentUser.Username,
Email: "newemail@coder.com",
})
@@ -281,28 +284,6 @@ func TestUpdateUserProfile(t *testing.T) {
require.Equal(t, userProfile.Username, me.Username)
require.Equal(t, userProfile.Email, "newemail@coder.com")
})
t.Run("KeepUserName", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
me, _ := client.User(context.Background(), codersdk.Me)
newName := "New Name"
firstProfile, _ := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
Username: me.Username,
Email: me.Email,
Name: &newName,
})
t.Log(firstProfile)
userProfile, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
Username: "newusername",
Email: "newemail@coder.com",
})
require.NoError(t, err)
require.Equal(t, userProfile.Username, "newusername")
require.Equal(t, userProfile.Email, "newemail@coder.com")
require.Equal(t, userProfile.Name, newName)
})
}
func TestUserByName(t *testing.T) {
@@ -318,12 +299,13 @@ func TestGetUsers(t *testing.T) {
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
client.CreateUser(context.Background(), codersdk.CreateUserRequest{
Email: "bruno@coder.com",
Username: "bruno",
Email: "alice@email.com",
Username: "alice",
Password: "password",
OrganizationID: user.OrganizationID,
})
users, err := client.GetUsers(context.Background())
// No params is all users
users, err := client.Users(context.Background(), codersdk.UsersRequest{})
require.NoError(t, err)
require.Len(t, users, 2)
}
@@ -546,3 +528,151 @@ func TestWorkspaceByUserAndName(t *testing.T) {
require.NoError(t, err)
})
}
// TestPaginatedUsers creates a list of users, then tries to paginate through
// them using different page sizes.
func TestPaginatedUsers(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := coderdtest.New(t, &coderdtest.Options{APIRateLimit: -1})
coderdtest.CreateFirstUser(t, client)
me, err := client.User(context.Background(), codersdk.Me)
require.NoError(t, err)
allUsers := make([]codersdk.User, 0)
allUsers = append(allUsers, me)
specialUsers := make([]codersdk.User, 0)
org, err := client.CreateOrganization(ctx, me.ID, codersdk.CreateOrganizationRequest{
Name: "default",
})
require.NoError(t, err)
// When 100 users exist
total := 100
// Create users
for i := 0; i < total; i++ {
email := fmt.Sprintf("%d@coder.com", i)
username := fmt.Sprintf("user%d", i)
if i%2 == 0 {
email = fmt.Sprintf("%d@gmail.com", i)
username = fmt.Sprintf("specialuser%d", i)
}
// One side effect of having to use the api vs the db calls directly, is you cannot
// mock time. Ideally I could pass in mocked times and space these users out.
//
// But this also serves as a good test. Postgres has microsecond precision on its timestamps.
// If 2 users share the same created_at, that could cause an issue if you are strictly paginating via
// timestamps. The pagination goes by timestamps and uuids.
newUser, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
Email: email,
Username: username,
Password: "password",
OrganizationID: org.ID,
})
require.NoError(t, err)
allUsers = append(allUsers, newUser)
if i%2 == 0 {
specialUsers = append(specialUsers, newUser)
}
}
// Sorting the users will sort by (created_at, uuid). This is to handle
// the off case that created_at is identical for 2 users.
// This is a really rare case in production, but does happen in unit tests
// due to the fake database being in memory and exceptionally quick.
sortUsers(allUsers)
sortUsers(specialUsers)
assertPagination(ctx, t, client, 10, allUsers, nil)
assertPagination(ctx, t, client, 5, allUsers, nil)
assertPagination(ctx, t, client, 3, allUsers, nil)
assertPagination(ctx, t, client, 1, allUsers, nil)
// Try a search
gmailSearch := func(request codersdk.UsersRequest) codersdk.UsersRequest {
request.Search = "gmail"
return request
}
assertPagination(ctx, t, client, 3, specialUsers, gmailSearch)
assertPagination(ctx, t, client, 7, specialUsers, gmailSearch)
usernameSearch := func(request codersdk.UsersRequest) codersdk.UsersRequest {
request.Search = "specialuser"
return request
}
assertPagination(ctx, t, client, 3, specialUsers, usernameSearch)
assertPagination(ctx, t, client, 1, specialUsers, usernameSearch)
}
// Assert pagination will page through the list of all users using the given
// limit for each page. The 'allUsers' is the expected full list to compare
// against.
func assertPagination(ctx context.Context, t *testing.T, client *codersdk.Client, limit int, allUsers []codersdk.User,
opt func(request codersdk.UsersRequest) codersdk.UsersRequest) {
var count int
if opt == nil {
opt = func(request codersdk.UsersRequest) codersdk.UsersRequest {
return request
}
}
// Check the first page
page, err := client.Users(ctx, opt(codersdk.UsersRequest{
Limit: limit,
}))
require.NoError(t, err, "first page")
require.Equalf(t, page, allUsers[:limit], "first page, limit=%d", limit)
count += len(page)
for {
if len(page) == 0 {
break
}
afterCursor := page[len(page)-1].ID
// Assert each page is the next expected page
// This is using a cursor, and only works if all users created_at
// is unique.
page, err = client.Users(ctx, opt(codersdk.UsersRequest{
Limit: limit,
AfterUser: afterCursor,
}))
require.NoError(t, err, "next cursor page")
// Also check page by offset
offsetPage, err := client.Users(ctx, opt(codersdk.UsersRequest{
Limit: limit,
Offset: count,
}))
require.NoError(t, err, "next offset page")
var expected []codersdk.User
if count+limit > len(allUsers) {
expected = allUsers[count:]
} else {
expected = allUsers[count : count+limit]
}
require.Equalf(t, page, expected, "next users, after=%s, limit=%d", afterCursor, limit)
require.Equalf(t, offsetPage, expected, "offset users, offset=%d, limit=%d", count, limit)
// Also check the before
prevPage, err := client.Users(ctx, opt(codersdk.UsersRequest{
Offset: count - limit,
Limit: limit,
}))
require.NoError(t, err, "prev page")
require.Equal(t, allUsers[count-limit:count], prevPage, "prev users")
count += len(page)
}
}
// sortUsers sorts by (created_at, id)
func sortUsers(users []codersdk.User) {
sort.Slice(users, func(i, j int) bool {
if users[i].CreatedAt.Equal(users[j].CreatedAt) {
return users[i].ID.String() < users[j].ID.String()
}
return users[i].CreatedAt.Before(users[j].CreatedAt)
})
}
+1 -1
View File
@@ -166,7 +166,7 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
priorHistory, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
if err == nil {
priorJob, err := api.Database.GetProvisionerJobByID(r.Context(), priorHistory.JobID)
if err == nil && !priorJob.CompletedAt.Valid {
if err == nil && convertProvisionerJob(priorJob).Status.Active() {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: "a workspace build is already active",
})
+1
View File
@@ -19,6 +19,7 @@ type GitSSHKey struct {
}
type AgentGitSSHKey struct {
PublicKey string `json:"public_key"`
PrivateKey string `json:"private_key"`
}
+11 -2
View File
@@ -25,6 +25,15 @@ type ProvisionerDaemon database.ProvisionerDaemon
// ProvisionerJobStaus represents the at-time state of a job.
type ProvisionerJobStatus string
// Active returns whether the job is still active or not.
// It returns true if canceling as well, since the job isn't
// in an entirely inactive state yet.
func (p ProvisionerJobStatus) Active() bool {
return p == ProvisionerJobPending ||
p == ProvisionerJobRunning ||
p == ProvisionerJobCanceling
}
const (
ProvisionerJobPending ProvisionerJobStatus = "pending"
ProvisionerJobRunning ProvisionerJobStatus = "running"
@@ -70,8 +79,8 @@ func (c *Client) ListenProvisionerDaemon(ctx context.Context) (proto.DRPCProvisi
}
return nil, readBodyAsError(res)
}
// Allow _somewhat_ large payloads.
conn.SetReadLimit((1 << 20) * 2)
// Align with the frame size of yamux.
conn.SetReadLimit(256 * 1024)
config := yamux.DefaultConfig()
config.LogOutput = io.Discard
+67 -17
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"github.com/google/uuid"
@@ -13,13 +14,26 @@ import (
// Me is used as a replacement for your own ID.
var Me = uuid.Nil
type UsersRequest struct {
AfterUser uuid.UUID `json:"after_user"`
Search string `json:"search"`
// Limit sets the maximum number of users to be returned
// in a single page. If the limit is <= 0, there is no limit
// and all users are returned.
Limit int `json:"limit"`
// Offset is used to indicate which page to return. An offset of 0
// returns the first 'limit' number of users.
// To get the next page, use offset=<limit>*<page_number>.
// Offset is 0 indexed, so the first record sits at offset 0.
Offset int `json:"offset"`
}
// User represents a user in Coder.
type User struct {
ID uuid.UUID `json:"id" validate:"required"`
Email string `json:"email" validate:"required"`
CreatedAt time.Time `json:"created_at" validate:"required"`
Username string `json:"username" validate:"required"`
Name string `json:"name"`
}
type CreateFirstUserRequest struct {
@@ -43,9 +57,8 @@ type CreateUserRequest struct {
}
type UpdateUserProfileRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,username"`
Name *string `json:"name"`
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,username"`
}
// LoginWithPasswordRequest enables callers to authenticate with email and password.
@@ -77,6 +90,12 @@ type CreateWorkspaceRequest struct {
ParameterValues []CreateParameterRequest `json:"parameter_values"`
}
// AuthMethods contains whether authentication types are enabled or not.
type AuthMethods struct {
Password bool `json:"password"`
Github bool `json:"github"`
}
// HasFirstUser returns whether the first user has been created.
func (c *Client) HasFirstUser(ctx context.Context) (bool, error) {
res, err := c.request(ctx, http.MethodGet, "/api/v2/users/first", nil)
@@ -136,19 +155,6 @@ func (c *Client) UpdateUserProfile(ctx context.Context, userID uuid.UUID, req Up
return user, json.NewDecoder(res.Body).Decode(&user)
}
func (c *Client) GetUsers(ctx context.Context) ([]User, error) {
res, err := c.request(ctx, http.MethodGet, "/api/v2/users", nil)
if err != nil {
return []User{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return []User{}, readBodyAsError(res)
}
var users []User
return users, json.NewDecoder(res.Body).Decode(&users)
}
// CreateAPIKey generates an API key for the user ID provided.
func (c *Client) CreateAPIKey(ctx context.Context, userID uuid.UUID) (*GenerateAPIKeyResponse, error) {
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", uuidOrMe(userID)), nil)
@@ -210,6 +216,34 @@ func (c *Client) User(ctx context.Context, id uuid.UUID) (User, error) {
return user, json.NewDecoder(res.Body).Decode(&user)
}
// Users returns all users according to the request parameters. If no parameters are set,
// the default behavior is to return all users in a single page.
func (c *Client) Users(ctx context.Context, req UsersRequest) ([]User, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users"), nil, func(r *http.Request) {
q := r.URL.Query()
if req.AfterUser != uuid.Nil {
q.Set("after_user", req.AfterUser.String())
}
if req.Limit > 0 {
q.Set("limit", strconv.Itoa(req.Limit))
}
q.Set("offset", strconv.Itoa(req.Offset))
q.Set("search", req.Search)
r.URL.RawQuery = q.Encode()
})
if err != nil {
return []User{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return []User{}, readBodyAsError(res)
}
var users []User
return users, json.NewDecoder(res.Body).Decode(&users)
}
// OrganizationsByUser returns all organizations the user is a member of.
func (c *Client) OrganizationsByUser(ctx context.Context, userID uuid.UUID) ([]Organization, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations", uuidOrMe(userID)), nil)
@@ -300,6 +334,22 @@ func (c *Client) WorkspaceByName(ctx context.Context, userID uuid.UUID, name str
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
}
// AuthMethods returns types of authentication available to the user.
func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) {
res, err := c.request(ctx, http.MethodGet, "/api/v2/users/authmethods", nil)
if err != nil {
return AuthMethods{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return AuthMethods{}, readBodyAsError(res)
}
var userAuth AuthMethods
return userAuth, json.NewDecoder(res.Body).Decode(&userAuth)
}
// uuidOrMe returns the provided uuid as a string if it's valid, ortherwise
// `me`.
func uuidOrMe(id uuid.UUID) string {
+1 -1
View File
@@ -14,6 +14,6 @@ cd "${PROJECT_ROOT}"
(
trap 'kill 0' SIGINT
CODERV2_HOST=http://127.0.0.1:3000 INSPECT_XSTATE=true yarn --cwd=./site dev &
go run cmd/coder/main.go start --dev --skip-tunnel &
go run cmd/coder/main.go server --dev --skip-tunnel &
wait
)
+2 -2
View File
@@ -17,14 +17,14 @@ Install [the latest release](https://github.com/coder/coder/releases).
To tinker, start with dev-mode (all data is in-memory, and is destroyed on exit):
```bash
$ coder start --dev
$ coder server --dev
```
To run a production deployment with PostgreSQL:
```bash
$ CODER_PG_CONNECTION_URL="postgres://<username>@<host>/<database>?password=<password>" \
coder start
coder server
```
To run as a system service, install with `.deb` or `.rpm`:
+20 -15
View File
@@ -17,6 +17,9 @@ replace github.com/chzyer/readline => github.com/kylecarbs/readline v0.0.0-20220
// Required until https://github.com/briandowns/spinner/pull/136 is merged.
replace github.com/briandowns/spinner => github.com/kylecarbs/spinner v1.18.2-0.20220329160715-20702b5af89e
// Required until https://github.com/storj/drpc/pull/31 is merged.
replace storj.io/drpc => github.com/kylecarbs/drpc v0.0.31-0.20220424193521-8ebbaf48bdff
// opencensus-go leaks a goroutine by default.
replace go.opencensus.io => github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b
@@ -38,7 +41,7 @@ replace github.com/fatedier/kcp-go => github.com/coder/kcp-go v2.0.4-0.202204091
require (
cdr.dev/slog v1.4.1
cloud.google.com/go/compute v1.6.0
cloud.google.com/go/compute v1.6.1
github.com/AlecAivazis/survey/v2 v2.3.4
github.com/awalterschulze/gographviz v2.0.3+incompatible
github.com/bgentry/speakeasy v0.1.0
@@ -49,7 +52,7 @@ require (
github.com/coder/retry v1.3.0
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/creack/pty v1.1.18
github.com/fatedier/frp v0.36.2-0.20220414032436-21240ed96251
github.com/fatedier/frp v0.42.0
github.com/fatedier/golib v0.1.1-0.20220321042308-c306138b83ac
github.com/fatih/color v1.13.0
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa
@@ -58,9 +61,10 @@ require (
github.com/go-chi/httprate v0.5.3
github.com/go-chi/render v1.0.1
github.com/go-playground/validator/v10 v10.10.1
github.com/gohugoio/hugo v0.97.2
github.com/gohugoio/hugo v0.97.3
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang-migrate/migrate/v4 v4.15.1
github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405
github.com/google/uuid v1.3.0
github.com/hashicorp/go-version v1.4.0
github.com/hashicorp/hc-install v0.3.1
@@ -74,7 +78,7 @@ require (
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
github.com/lib/pq v1.10.5
github.com/mattn/go-isatty v0.0.14
github.com/mitchellh/mapstructure v1.4.3
github.com/mitchellh/mapstructure v1.5.0
github.com/moby/moby v20.10.14+incompatible
github.com/open-policy-agent/opa v0.39.0
github.com/ory/dockertest/v3 v3.8.1
@@ -82,7 +86,7 @@ require (
github.com/pion/logging v0.2.2
github.com/pion/transport v0.13.0
github.com/pion/turn/v2 v2.0.8
github.com/pion/webrtc/v3 v3.1.29
github.com/pion/webrtc/v3 v3.1.34
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/pkg/sftp v1.13.4
github.com/quasilyte/go-ruleguard/dsl v0.3.19
@@ -95,18 +99,18 @@ require (
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1
go.uber.org/atomic v1.9.0
go.uber.org/goleak v1.1.12
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a
golang.org/x/net v0.0.0-20220412020605-290c469a71a5
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
google.golang.org/api v0.74.0
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f
google.golang.org/api v0.75.0
google.golang.org/protobuf v1.28.0
gopkg.in/DataDog/dd-trace-go.v1 v1.37.1
gopkg.in/DataDog/dd-trace-go.v1 v1.38.0
nhooyr.io/websocket v1.8.7
storj.io/drpc v0.0.30
)
@@ -157,6 +161,7 @@ require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.7 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
@@ -190,7 +195,7 @@ require (
github.com/pelletier/go-toml/v2 v2.0.0-beta.7.0.20220408132554-2377ac4bc04c // indirect
github.com/philhofer/fwd v1.1.1 // indirect
github.com/pion/dtls/v2 v2.1.3 // indirect
github.com/pion/ice/v2 v2.2.4 // indirect
github.com/pion/ice/v2 v2.2.6 // indirect
github.com/pion/interceptor v0.1.10 // indirect
github.com/pion/mdns v0.0.5 // indirect
github.com/pion/randutil v0.1.0 // indirect
@@ -201,7 +206,7 @@ require (
github.com/pion/srtp/v2 v2.0.5 // indirect
github.com/pion/stun v0.3.5 // indirect
github.com/pion/udp v0.1.1 // indirect
github.com/pires/go-proxyproto v0.5.0 // indirect
github.com/pires/go-proxyproto v0.6.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pquerna/cachecontrol v0.1.0 // indirect
@@ -225,7 +230,7 @@ require (
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9 // indirect
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3 // indirect
google.golang.org/grpc v1.45.0 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
+38 -71
View File
@@ -43,8 +43,9 @@ cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM7
cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
cloud.google.com/go/compute v1.6.0 h1:XdQIN5mdPTSBVwSIVDuY5e8ZzVAccsHvD3qTEz4zIps=
cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
cloud.google.com/go/compute v1.6.1 h1:2sMmt8prCn7DPaG4Pmh0N3Inmc8cT8ae5k1M6VJ9Wqc=
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
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=
@@ -141,7 +142,6 @@ github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
@@ -170,7 +170,6 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andybalholm/crlf v0.0.0-20171020200849-670099aa064f/go.mod h1:k8feO4+kXDxro6ErPXBRTJ/ro2mf0SsFG8s7doP9kJE=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -187,7 +186,6 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
@@ -224,7 +222,6 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.7.2/go.mod h1:np7TMuJN
github.com/aws/aws-sdk-go-v2/service/s3 v1.12.0/go.mod h1:6J++A5xpo7QDsIeSqPK4UHqMSyPOCopa+zKtqAMhqVQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.16.1/go.mod h1:CQe/KvWV1AqRc65KqeJjrLzr5X2ijnFTTVzJW0VBRCI=
github.com/aws/aws-sdk-go-v2/service/sqs v1.0.0/go.mod h1:w5BclCU8ptTbagzXS/fHBr+vAyXUjggg/72qDIURKMk=
github.com/aws/aws-sdk-go-v2/service/sso v1.0.0/go.mod h1:qNdDupP6xoM//zL1JmPl2XGbyPL5kKrlsoYVh8XZxzQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.3.2/go.mod h1:J21I6kF+d/6XHVk7kp/cx9YVD2TMD2TbLwtRGVcinXo=
github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk=
github.com/aws/aws-sdk-go-v2/service/sts v1.0.0/go.mod h1:5f+cELGATgill5Pu3/vK3Ebuigstc+qYEHW5MvGWZO4=
@@ -258,7 +255,6 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bytecodealliance/wasmtime-go v0.35.0 h1:VZjaZ0XOY0qp9TQfh0CQj9zl/AbdeXePVTALy8V1sKs=
github.com/bytecodealliance/wasmtime-go v0.35.0/go.mod h1:q320gUxqyI8yB+ZqRuaJOEnGkAnHh6WtJjMaT2CW4wI=
github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo=
@@ -343,7 +339,6 @@ github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.
github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
@@ -360,7 +355,6 @@ github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL
github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20200709052629-daa8e1ccc0bc/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=
github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=
github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y=
github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=
@@ -483,7 +477,6 @@ github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.0+incompatible h1:l9EaZDICImO1ngI+uTifW+ZYvvz7fKISBAKpg+MbWbY=
github.com/docker/distribution v2.8.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.4.2-0.20200319182547-c7ad2b866182/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.12+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
@@ -519,7 +512,6 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
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.8/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
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/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
@@ -528,11 +520,10 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb h1:wCrNShQidLmvVWn/0PikGmpdP0vtQmnvyRg3ZBEhczw=
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb/go.mod h1:wx3gB6dbIfBRcucp94PI9Bt3I0F2c/MyNEWuhzpWiwk=
github.com/fatedier/frp v0.36.2-0.20220414032436-21240ed96251 h1:IeF8g33NNx6i2YKporO9FJ8R9FL7n2TXzlkdyiHx+dY=
github.com/fatedier/frp v0.36.2-0.20220414032436-21240ed96251/go.mod h1:3Mp3fWvK8kD3PU8Hy89fKV/655Q5eFSHr+uuUr/lbOg=
github.com/fatedier/frp v0.42.0 h1:IIPCKB5OgGetjIk7vv3MlR3iL8qS0d7uM3kjWs6eymU=
github.com/fatedier/frp v0.42.0/go.mod h1:NahedvXauelo3mcioq3gahG3BdhTfJ4Gia6rRQnE02A=
github.com/fatedier/golib v0.1.1-0.20220321042308-c306138b83ac h1:td1FJwN/oz8+9GldeEm3YdBX0Husc0FSPywLesZxi4w=
github.com/fatedier/golib v0.1.1-0.20220321042308-c306138b83ac/go.mod h1:fLV0TLwHqrnB/L3jbNl67Gn6PCLggDGHniX1wLrA2Qo=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -578,7 +569,6 @@ github.com/gliderlabs/ssh v0.3.3/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQK
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi v1.5.0/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k=
github.com/go-chi/chi/v4 v4.0.0-rc1/go.mod h1:Yfiy+5nynjDc7IMJiguACIro1KxlGW2dLUqcroaEUEY=
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
@@ -607,9 +597,7 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8=
github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
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-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
@@ -713,8 +701,8 @@ github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/gohugoio/hugo v0.97.2 h1:KpmX9BvVXfuBdJqauSR2bcU1BWlyL4RYkaJVHjProoU=
github.com/gohugoio/hugo v0.97.2/go.mod h1:FRv84nbwdRigH4boW1lxmBvHt2+ZLYu8W9wNuVOBeAo=
github.com/gohugoio/hugo v0.97.3 h1:m8DRdCwdxTxAEGOSemHbapkjxrfo/l+d5pCAc8kz4Y4=
github.com/gohugoio/hugo v0.97.3/go.mod h1:eK1apZTr4Kt04LwYgVFg7h0dXIYJyESgPb4jL2SX8lk=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-migrate/migrate/v4 v4.15.1 h1:Sakl3Nm6+wQKq0Q62tpFMi5a503bgGhceo2icrgQ9vM=
@@ -784,7 +772,11 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-github/v35 v35.2.0/go.mod h1:s0515YVTI+IMrDoy9Y4pHt9ShGpzHvHO8rZ7L7acgvs=
github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405 h1:DdHws/YnnPrSywrjNYu2lEHqYHWp/LnEx56w59esd54=
github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405/go.mod h1:4RgUDSnsxP19d65zJWqvqJ/poJxBCvmna50eXmIvoR8=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -853,7 +845,6 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de
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.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.15.2/go.mod h1:vO11I9oWA+KsxmfFQPhLnnIb1VDE24M+pdxZFiuZcA8=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
@@ -861,7 +852,6 @@ github.com/hashicorp/consul/api v1.0.0/go.mod h1:mbFwfRxOTDHZpT3iUsMAFcLNoVm6Xbe
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/internal v0.1.0/go.mod h1:zi9bMZYbiPHyAjgBWo7kCUcy5l2NrTdrkVupCc7Oo6c=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/consul/sdk v0.7.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM=
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
@@ -874,7 +864,6 @@ github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/S
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
@@ -886,17 +875,10 @@ github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=
github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
@@ -904,7 +886,6 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4=
github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
@@ -922,7 +903,6 @@ github.com/hashicorp/hcl/v2 v2.11.1 h1:yTyWcXcm9XB0TEkyU/JCRU6rYy4K+mgLtzn2wlrJb
github.com/hashicorp/hcl/v2 v2.11.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
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/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/memberlist v0.1.6/go.mod h1:5VDNHjqFMgEcclnwmkCnC99IPwxBmIsxwY8qn+Nl0H4=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
@@ -932,7 +912,6 @@ github.com/hashicorp/terraform-json v0.13.0/go.mod h1:y5OdLBCT+rxbwnpxZs9kGL7R9E
github.com/hashicorp/vault/api v1.1.0/go.mod h1:R3Umvhlxi2TN7Ex2hzOowyeNb+SfbVWI973N+ctaFMk=
github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I=
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
@@ -1013,9 +992,7 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jedib0t/go-pretty/v6 v6.3.1 h1:aOXiD9oqiuLH8btPQW6SfgtQN5zwhyfzZls8a6sPJ/I=
github.com/jedib0t/go-pretty/v6 v6.3.1/go.mod h1:FMkOpgGD3EZ91cW8g/96RfxoV7bdeJyzXPYgz1L1ln0=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
github.com/jinzhu/gorm v1.9.1/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
@@ -1082,7 +1059,6 @@ github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwc
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid/v2 v2.0.6 h1:dQ5ueTiftKxp0gyjKSx5+8BtPWkyQbd95m8Gys/RarI=
github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/crc32 v1.2.0/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg=
github.com/klauspost/reedsolomon v1.9.15 h1:g2erWKD2M6rgnPf89fCji6jNlhMKMdXcuNHMW1SYCIo=
github.com/klauspost/reedsolomon v1.9.15/go.mod h1:eqPAcE7xar5CIzcdfwydOEdcmchAKAP/qs14y4GCBOk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -1103,6 +1079,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4=
github.com/kylecarbs/drpc v0.0.31-0.20220424193521-8ebbaf48bdff h1:7qg425aXdULnZWCCQNPOzHO7c+M6BpbTfOUJLrk5+3w=
github.com/kylecarbs/drpc v0.0.31-0.20220424193521-8ebbaf48bdff/go.mod h1:6rcOyR/QQkSTX/9L5ZGtlZaE2PtXTTZl8d+ulSeeYEg=
github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b h1:1Y1X6aR78kMEQE1iCjQodB3lA7VO4jB88Wf8ZrzXSsA=
github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
github.com/kylecarbs/readline v0.0.0-20220211054233-0d62993714c8/go.mod h1:n/KX1BZoN1m9EwoXkn/xAV4fd3k8c++gGBsgLONaPOY=
@@ -1195,7 +1173,6 @@ github.com/miekg/dns v1.1.45/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7Xn
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -1215,8 +1192,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
@@ -1356,9 +1333,8 @@ github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
github.com/pion/dtls/v2 v2.1.3 h1:3UF7udADqous+M2R5Uo2q/YaP4EzUoWKdfX2oscCUio=
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
github.com/pion/ice/v2 v2.2.3/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
github.com/pion/ice/v2 v2.2.4 h1:sTHT39ywr5uqzyEMT7thEhOWsNOcdkHSZBbgQohFuZU=
github.com/pion/ice/v2 v2.2.4/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
github.com/pion/interceptor v0.1.10 h1:DJ2GjMGm4XGIQgMJxuEpdaExdY/6RdngT7Uh4oVmquU=
github.com/pion/interceptor v0.1.10/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
@@ -1391,10 +1367,10 @@ github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
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.1.29 h1:X/2LbFzBhU2h335azBGmdmrRZIChWTePrg4rwIw91ko=
github.com/pion/webrtc/v3 v3.1.29/go.mod h1:bcD6vrgcflr6lkf3E8VEqnQT7Uf7y1AxcdUWYGKER1w=
github.com/pires/go-proxyproto v0.5.0 h1:A4Jv4ZCaV3AFJeGh5mGwkz4iuWUYMlQ7IoO/GTuSuLo=
github.com/pires/go-proxyproto v0.5.0/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pion/webrtc/v3 v3.1.34 h1:GUfv2zxWge77x1FhZ6Fge8KQd3bTzvX1CMN1/LmTazM=
github.com/pion/webrtc/v3 v3.1.34/go.mod h1:jClfnbJzt8wtmewGxhPzgE5wZ0U/gNB78XCtmc/uz3k=
github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
@@ -1413,7 +1389,6 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
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/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
@@ -1517,14 +1492,12 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/gunit v1.1.3/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
github.com/snowflakedb/gosnowflake v1.6.3/go.mod h1:6hLajn6yxuJ4xUHZegMekpq9rnQbGJ7TMwXjgTmA6lg=
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=
@@ -1584,25 +1557,20 @@ github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7S
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU=
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b h1:fj5tQ8acgNUr6O8LEplsxDhUIe2573iLkJc+PqnzZTI=
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4=
github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8=
github.com/tidwall/btree v0.3.0/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
github.com/tidwall/btree v1.1.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4=
github.com/tidwall/buntdb v1.2.0/go.mod h1:XLza/dhlwzO6dc5o/KWor4kfZSt3BP8QV+77ZMKfI58=
github.com/tidwall/cities v0.1.0/go.mod h1:lV/HDp2gCcRcHJWqgt6Di54GiDrTZwh1aG2ZUPNbqa4=
github.com/tidwall/geoindex v1.4.4/go.mod h1:rvVVNEFfkJVWGUdEfU8QaoOg/9zFX0h9ofWzA60mz1I=
github.com/tidwall/gjson v1.6.7/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI=
github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/grect v0.1.0/go.mod h1:sa5O42oP6jWfTShL9ka6Sgmg3TgIK649veZe05B7+J8=
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
github.com/tidwall/rtree v1.3.1/go.mod h1:S+JSsqPTI8LfWA4xHBo5eXzie8WJLVFeppAutSegl6M=
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ=
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
@@ -1773,8 +1741,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1831,7 +1799,6 @@ golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdx
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1842,7 +1809,6 @@ golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73r
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-20190108155000-395948e2f546/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-20190225153610-fe579d43d832/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1859,7 +1825,6 @@ golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -1910,8 +1875,9 @@ golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd h1:zYlwaUHTmxuf6H7hwO2dgwqozQmH7zf4x+/qql4oVWc=
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -1931,8 +1897,9 @@ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
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=
@@ -2080,8 +2047,9 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886 h1:eJv7u3ksNXoLbGSKuv2s/SIO4tJVxc/A+MTpzxDgz/Q=
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -2194,8 +2162,9 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T
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=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
@@ -2238,8 +2207,9 @@ google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tD
google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
google.golang.org/api v0.74.0 h1:ExR2D+5TYIrMphWgs5JCgwRhEDlPDXXrLwHHMgPHTXE=
google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
google.golang.org/api v0.75.0 h1:0AYh/ae6l9TDUvIQrDw5QRpM100P6oHgD+o3dYHMzJg=
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -2251,7 +2221,6 @@ google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
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=
@@ -2332,10 +2301,11 @@ google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9 h1:XGQ6tc+EnM35IAazg4y6AHmUg4oK8NXsXaILte1vRlk=
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3 h1:SeX3QUcBj3fciwnfPT9kt5gBhFy/FCZtYZ+I/RB8agc=
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -2390,11 +2360,10 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/DataDog/dd-trace-go.v1 v1.37.1 h1:rgO9oC3Mr7es0zKIsaooL50UY0qboUDdueP7MzH1fUI=
gopkg.in/DataDog/dd-trace-go.v1 v1.37.1/go.mod h1:HMpV5TQ38YAfRJ8OlM7UfLyGg6D01MPNHRFwsnrGMB0=
gopkg.in/DataDog/dd-trace-go.v1 v1.38.0 h1:vm/mYIZCEp5j2MoKPmwM3t6EGthxpvVbCOm2hRl5uDc=
gopkg.in/DataDog/dd-trace-go.v1 v1.38.0/go.mod h1:GBhK4yaMJ1h329ivtKAqRNe1EZ944UnZwtz5lh7CnJc=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -2540,5 +2509,3 @@ sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK
sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
storj.io/drpc v0.0.30 h1:jqPe4T9KEu3CDBI05A2hCMgMSHLtd/E0N0yTF9QreIE=
storj.io/drpc v0.0.30/go.mod h1:6rcOyR/QQkSTX/9L5ZGtlZaE2PtXTTZl8d+ulSeeYEg=
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v3.19.4
// protoc v3.20.0
// source: peerbroker/proto/peerbroker.proto
package proto
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v3.19.4
// protoc v3.20.0
// source: provisionerd/proto/provisionerd.proto
package proto
+135 -54
View File
@@ -68,8 +68,8 @@ func New(clientDialer Dialer, opts *Options) *Server {
clientDialer: clientDialer,
opts: opts,
closeCancel: ctxCancel,
closed: make(chan struct{}),
closeContext: ctx,
closeCancel: ctxCancel,
shutdown: make(chan struct{}),
@@ -87,13 +87,13 @@ type Server struct {
opts *Options
clientDialer Dialer
client proto.DRPCProvisionerDaemonClient
clientValue atomic.Value
// Locked when closing the daemon.
closeMutex sync.Mutex
closeCancel context.CancelFunc
closed chan struct{}
closeError error
closeMutex sync.Mutex
closeContext context.Context
closeCancel context.CancelFunc
closeError error
shutdownMutex sync.Mutex
shutdown chan struct{}
@@ -108,11 +108,10 @@ type Server struct {
// Connect establishes a connection to coderd.
func (p *Server) connect(ctx context.Context) {
var err error
// An exponential back-off occurs when the connection is failing to dial.
// This is to prevent server spam in case of a coderd outage.
for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(ctx); {
p.client, err = p.clientDialer(ctx)
client, err := p.clientDialer(ctx)
if err != nil {
if errors.Is(err, context.Canceled) {
return
@@ -126,6 +125,7 @@ func (p *Server) connect(ctx context.Context) {
p.closeMutex.Unlock()
continue
}
p.clientValue.Store(client)
p.opts.Logger.Debug(context.Background(), "connected")
break
}
@@ -139,10 +139,14 @@ func (p *Server) connect(ctx context.Context) {
if p.isClosed() {
return
}
select {
case <-p.closed:
client, ok := p.client()
if !ok {
return
case <-p.client.DRPCConn().Closed():
}
select {
case <-p.closeContext.Done():
return
case <-client.DRPCConn().Closed():
// We use the update stream to detect when the connection
// has been interrupted. This works well, because logs need
// to buffer if a job is running in the background.
@@ -158,10 +162,14 @@ func (p *Server) connect(ctx context.Context) {
ticker := time.NewTicker(p.opts.PollInterval)
defer ticker.Stop()
for {
select {
case <-p.closed:
client, ok := p.client()
if !ok {
return
case <-p.client.DRPCConn().Closed():
}
select {
case <-p.closeContext.Done():
return
case <-client.DRPCConn().Closed():
return
case <-ticker.C:
p.acquireJob(ctx)
@@ -170,6 +178,15 @@ func (p *Server) connect(ctx context.Context) {
}()
}
func (p *Server) client() (proto.DRPCProvisionerDaemonClient, bool) {
rawClient := p.clientValue.Load()
if rawClient == nil {
return nil, false
}
client, ok := rawClient.(proto.DRPCProvisionerDaemonClient)
return client, ok
}
func (p *Server) isRunningJob() bool {
select {
case <-p.jobRunning:
@@ -195,7 +212,11 @@ func (p *Server) acquireJob(ctx context.Context) {
return
}
var err error
job, err := p.client.AcquireJob(ctx, &proto.Empty{})
client, ok := p.client()
if !ok {
return
}
job, err := client.AcquireJob(ctx, &proto.Empty{})
if err != nil {
if errors.Is(err, context.Canceled) {
return
@@ -226,24 +247,36 @@ func (p *Server) acquireJob(ctx context.Context) {
func (p *Server) runJob(ctx context.Context, job *proto.AcquiredJob) {
shutdown, shutdownCancel := context.WithCancel(ctx)
defer shutdownCancel()
complete, completeCancel := context.WithCancel(ctx)
defer completeCancel()
go func() {
ticker := time.NewTicker(p.opts.UpdateInterval)
defer ticker.Stop()
for {
select {
case <-p.closed:
case <-p.closeContext.Done():
return
case <-ctx.Done():
return
case <-complete.Done():
return
case <-p.shutdown:
p.opts.Logger.Info(ctx, "attempting graceful cancelation")
shutdownCancel()
return
case <-ticker.C:
}
resp, err := p.client.UpdateJob(ctx, &proto.UpdateJobRequest{
client, ok := p.client()
if !ok {
continue
}
resp, err := client.UpdateJob(ctx, &proto.UpdateJobRequest{
JobId: job.JobId,
})
if errors.Is(err, yamux.ErrSessionShutdown) || errors.Is(err, io.EOF) {
continue
}
if err != nil {
p.failActiveJobf("send periodic update: %s", err)
return
@@ -297,7 +330,12 @@ func (p *Server) runJob(ctx context.Context, job *proto.AcquiredJob) {
return
}
_, err = p.client.UpdateJob(ctx, &proto.UpdateJobRequest{
client, ok := p.client()
if !ok {
p.failActiveJobf("client disconnected")
return
}
_, err = client.UpdateJob(ctx, &proto.UpdateJobRequest{
JobId: job.GetJobId(),
Logs: []*proto.Log{{
Source: proto.LogSource_PROVISIONER_DAEMON,
@@ -387,10 +425,14 @@ func (p *Server) runJob(ctx context.Context, job *proto.AcquiredJob) {
return
}
client, ok = p.client()
if !ok {
return
}
// Ensure the job is still running to output.
// It's possible the job has failed.
if p.isRunningJob() {
_, err = p.client.UpdateJob(ctx, &proto.UpdateJobRequest{
_, err = client.UpdateJob(ctx, &proto.UpdateJobRequest{
JobId: job.GetJobId(),
Logs: []*proto.Log{{
Source: proto.LogSource_PROVISIONER_DAEMON,
@@ -409,7 +451,12 @@ func (p *Server) runJob(ctx context.Context, job *proto.AcquiredJob) {
}
func (p *Server) runTemplateImport(ctx, shutdown context.Context, provisioner sdkproto.DRPCProvisionerClient, job *proto.AcquiredJob) {
_, err := p.client.UpdateJob(ctx, &proto.UpdateJobRequest{
client, ok := p.client()
if !ok {
p.failActiveJobf("client disconnected")
return
}
_, err := client.UpdateJob(ctx, &proto.UpdateJobRequest{
JobId: job.GetJobId(),
Logs: []*proto.Log{{
Source: proto.LogSource_PROVISIONER_DAEMON,
@@ -429,7 +476,7 @@ func (p *Server) runTemplateImport(ctx, shutdown context.Context, provisioner sd
return
}
updateResponse, err := p.client.UpdateJob(ctx, &proto.UpdateJobRequest{
updateResponse, err := client.UpdateJob(ctx, &proto.UpdateJobRequest{
JobId: job.JobId,
ParameterSchemas: parameterSchemas,
})
@@ -450,7 +497,7 @@ func (p *Server) runTemplateImport(ctx, shutdown context.Context, provisioner sd
}
}
_, err = p.client.UpdateJob(ctx, &proto.UpdateJobRequest{
_, err = client.UpdateJob(ctx, &proto.UpdateJobRequest{
JobId: job.GetJobId(),
Logs: []*proto.Log{{
Source: proto.LogSource_PROVISIONER_DAEMON,
@@ -471,7 +518,7 @@ func (p *Server) runTemplateImport(ctx, shutdown context.Context, provisioner sd
p.failActiveJobf("template import provision for start: %s", err)
return
}
_, err = p.client.UpdateJob(ctx, &proto.UpdateJobRequest{
_, err = client.UpdateJob(ctx, &proto.UpdateJobRequest{
JobId: job.GetJobId(),
Logs: []*proto.Log{{
Source: proto.LogSource_PROVISIONER_DAEMON,
@@ -493,7 +540,7 @@ func (p *Server) runTemplateImport(ctx, shutdown context.Context, provisioner sd
return
}
_, err = p.client.CompleteJob(ctx, &proto.CompletedJob{
p.completeJob(&proto.CompletedJob{
JobId: job.JobId,
Type: &proto.CompletedJob_TemplateImport_{
TemplateImport: &proto.CompletedJob_TemplateImport{
@@ -502,14 +549,14 @@ func (p *Server) runTemplateImport(ctx, shutdown context.Context, provisioner sd
},
},
})
if err != nil {
p.failActiveJobf("complete job: %s", err)
return
}
}
// Parses parameter schemas from source.
func (p *Server) runTemplateImportParse(ctx context.Context, provisioner sdkproto.DRPCProvisionerClient, job *proto.AcquiredJob) ([]*sdkproto.ParameterSchema, error) {
client, ok := p.client()
if !ok {
return nil, xerrors.New("client disconnected")
}
stream, err := provisioner.Parse(ctx, &sdkproto.Parse_Request{
Directory: p.opts.WorkDirectory,
})
@@ -529,7 +576,7 @@ func (p *Server) runTemplateImportParse(ctx context.Context, provisioner sdkprot
slog.F("output", msgType.Log.Output),
)
_, err = p.client.UpdateJob(ctx, &proto.UpdateJobRequest{
_, err = client.UpdateJob(ctx, &proto.UpdateJobRequest{
JobId: job.JobId,
Logs: []*proto.Log{{
Source: proto.LogSource_PROVISIONER,
@@ -599,8 +646,11 @@ func (p *Server) runTemplateImportProvision(ctx, shutdown context.Context, provi
slog.F("level", msgType.Log.Level),
slog.F("output", msgType.Log.Output),
)
_, err = p.client.UpdateJob(ctx, &proto.UpdateJobRequest{
client, ok := p.client()
if !ok {
continue
}
_, err = client.UpdateJob(ctx, &proto.UpdateJobRequest{
JobId: job.JobId,
Logs: []*proto.Log{{
Source: proto.LogSource_PROVISIONER,
@@ -638,7 +688,12 @@ func (p *Server) runWorkspaceBuild(ctx, shutdown context.Context, provisioner sd
stage = "Destroying workspace"
}
_, err := p.client.UpdateJob(ctx, &proto.UpdateJobRequest{
client, ok := p.client()
if !ok {
p.failActiveJobf("client disconnected")
return
}
_, err := client.UpdateJob(ctx, &proto.UpdateJobRequest{
JobId: job.GetJobId(),
Logs: []*proto.Log{{
Source: proto.LogSource_PROVISIONER_DAEMON,
@@ -699,7 +754,7 @@ func (p *Server) runWorkspaceBuild(ctx, shutdown context.Context, provisioner sd
slog.F("workspace_build_id", job.GetWorkspaceBuild().WorkspaceBuildId),
)
_, err = p.client.UpdateJob(ctx, &proto.UpdateJobRequest{
_, err = client.UpdateJob(ctx, &proto.UpdateJobRequest{
JobId: job.JobId,
Logs: []*proto.Log{{
Source: proto.LogSource_PROVISIONER,
@@ -729,15 +784,7 @@ func (p *Server) runWorkspaceBuild(ctx, shutdown context.Context, provisioner sd
return
}
p.opts.Logger.Info(context.Background(), "provision successful; marking job as complete",
slog.F("resource_count", len(msgType.Complete.Resources)),
slog.F("resources", msgType.Complete.Resources),
slog.F("state_length", len(msgType.Complete.State)),
)
// Complete job may need to be async if we disconnected...
// When we reconnect we can flush any of these cached values.
_, err = p.client.CompleteJob(ctx, &proto.CompletedJob{
p.completeJob(&proto.CompletedJob{
JobId: job.JobId,
Type: &proto.CompletedJob_WorkspaceBuild_{
WorkspaceBuild: &proto.CompletedJob_WorkspaceBuild{
@@ -746,11 +793,12 @@ func (p *Server) runWorkspaceBuild(ctx, shutdown context.Context, provisioner sd
},
},
})
if err != nil {
p.failActiveJobf("complete job: %s", err)
return
}
// Return so we stop looping!
p.opts.Logger.Info(context.Background(), "provision successful; marked job as complete",
slog.F("resource_count", len(msgType.Complete.Resources)),
slog.F("resources", msgType.Complete.Resources),
slog.F("state_length", len(msgType.Complete.State)),
)
// Stop looping!
return
default:
p.failActiveJobf("invalid message type %T received from provisioner", msg.Type)
@@ -759,6 +807,27 @@ func (p *Server) runWorkspaceBuild(ctx, shutdown context.Context, provisioner sd
}
}
func (p *Server) completeJob(job *proto.CompletedJob) {
for retrier := retry.New(25*time.Millisecond, 5*time.Second); retrier.Wait(p.closeContext); {
client, ok := p.client()
if !ok {
continue
}
// Complete job may need to be async if we disconnected...
// When we reconnect we can flush any of these cached values.
_, err := client.CompleteJob(p.closeContext, job)
if xerrors.Is(err, yamux.ErrSessionShutdown) || xerrors.Is(err, io.EOF) {
continue
}
if err != nil {
p.opts.Logger.Warn(p.closeContext, "failed to complete job", slog.Error(err))
p.failActiveJobf(err.Error())
return
}
break
}
}
func (p *Server) failActiveJobf(format string, args ...interface{}) {
p.failActiveJob(&proto.FailedJob{
Error: fmt.Sprintf(format, args...),
@@ -786,18 +855,31 @@ func (p *Server) failActiveJob(failedJob *proto.FailedJob) {
slog.F("job_id", p.jobID),
)
failedJob.JobId = p.jobID
_, err := p.client.FailJob(context.Background(), failedJob)
if err != nil {
p.opts.Logger.Warn(context.Background(), "failed to notify of error; job is no longer running", slog.Error(err))
for retrier := retry.New(25*time.Millisecond, 5*time.Second); retrier.Wait(p.closeContext); {
client, ok := p.client()
if !ok {
continue
}
_, err := client.FailJob(p.closeContext, failedJob)
if xerrors.Is(err, yamux.ErrSessionShutdown) || xerrors.Is(err, io.EOF) {
continue
}
if err != nil {
if p.isClosed() {
return
}
p.opts.Logger.Warn(context.Background(), "failed to notify of error; job is no longer running", slog.Error(err))
return
}
p.opts.Logger.Debug(context.Background(), "marked running job as failed")
return
}
p.opts.Logger.Debug(context.Background(), "marked running job as failed")
}
// isClosed returns whether the API is closed or not.
func (p *Server) isClosed() bool {
select {
case <-p.closed:
case <-p.closeContext.Done():
return true
default:
return false
@@ -847,7 +929,6 @@ func (p *Server) closeWithError(err error) error {
return p.closeError
}
p.closeError = err
close(p.closed)
errMsg := "provisioner daemon was shutdown gracefully"
if err != nil {
+153 -8
View File
@@ -11,6 +11,7 @@ import (
"testing"
"time"
"github.com/hashicorp/yamux"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
"go.uber.org/goleak"
@@ -126,6 +127,7 @@ func TestProvisionerd(t *testing.T) {
// Ensures tars with "../../../etc/passwd" as the path
// are not allowed to run, and will fail the job.
t.Parallel()
var complete sync.Once
completeChan := make(chan struct{})
closer := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
return createProvisionerDaemonClient(t, provisionerDaemonTestServer{
@@ -145,7 +147,9 @@ func TestProvisionerd(t *testing.T) {
},
updateJob: noopUpdateJob,
failJob: func(ctx context.Context, job *proto.FailedJob) (*proto.Empty, error) {
close(completeChan)
complete.Do(func() {
close(completeChan)
})
return &proto.Empty{}, nil
},
}), nil
@@ -158,6 +162,7 @@ func TestProvisionerd(t *testing.T) {
t.Run("RunningPeriodicUpdate", func(t *testing.T) {
t.Parallel()
var complete sync.Once
completeChan := make(chan struct{})
closer := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
return createProvisionerDaemonClient(t, provisionerDaemonTestServer{
@@ -176,11 +181,9 @@ func TestProvisionerd(t *testing.T) {
}, nil
},
updateJob: func(ctx context.Context, update *proto.UpdateJobRequest) (*proto.UpdateJobResponse, error) {
select {
case <-completeChan:
default:
complete.Do(func() {
close(completeChan)
}
})
return &proto.UpdateJobResponse{}, nil
},
failJob: func(ctx context.Context, job *proto.FailedJob) (*proto.Empty, error) {
@@ -207,12 +210,15 @@ func TestProvisionerd(t *testing.T) {
didAcquireJob atomic.Bool
didDryRun atomic.Bool
)
var complete sync.Once
completeChan := make(chan struct{})
closer := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
return createProvisionerDaemonClient(t, provisionerDaemonTestServer{
acquireJob: func(ctx context.Context, _ *proto.Empty) (*proto.AcquiredJob, error) {
if didAcquireJob.Load() {
close(completeChan)
complete.Do(func() {
close(completeChan)
})
return &proto.AcquiredJob{}, nil
}
didAcquireJob.Store(true)
@@ -307,12 +313,15 @@ func TestProvisionerd(t *testing.T) {
didLog atomic.Bool
didAcquireJob atomic.Bool
)
var complete sync.Once
completeChan := make(chan struct{})
closer := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
return createProvisionerDaemonClient(t, provisionerDaemonTestServer{
acquireJob: func(ctx context.Context, _ *proto.Empty) (*proto.AcquiredJob, error) {
if didAcquireJob.Load() {
close(completeChan)
complete.Do(func() {
close(completeChan)
})
return &proto.AcquiredJob{}, nil
}
didAcquireJob.Store(true)
@@ -492,6 +501,7 @@ func TestProvisionerd(t *testing.T) {
t.Run("ShutdownFromJob", func(t *testing.T) {
t.Parallel()
var updated sync.Once
updateChan := make(chan struct{})
completeChan := make(chan struct{})
server := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
@@ -513,7 +523,9 @@ func TestProvisionerd(t *testing.T) {
updateJob: func(ctx context.Context, update *proto.UpdateJobRequest) (*proto.UpdateJobResponse, error) {
if len(update.Logs) > 0 && update.Logs[0].Source == proto.LogSource_PROVISIONER {
// Close on a log so we know when the job is in progress!
close(updateChan)
updated.Do(func() {
close(updateChan)
})
}
return &proto.UpdateJobResponse{
Canceled: true,
@@ -558,6 +570,139 @@ func TestProvisionerd(t *testing.T) {
<-completeChan
require.NoError(t, server.Close())
})
t.Run("ReconnectAndFail", func(t *testing.T) {
t.Parallel()
var second atomic.Bool
failChan := make(chan struct{})
failedChan := make(chan struct{})
completeChan := make(chan struct{})
server := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
client := createProvisionerDaemonClient(t, provisionerDaemonTestServer{
acquireJob: func(ctx context.Context, _ *proto.Empty) (*proto.AcquiredJob, error) {
if second.Load() {
return &proto.AcquiredJob{}, nil
}
return &proto.AcquiredJob{
JobId: "test",
Provisioner: "someprovisioner",
TemplateSourceArchive: createTar(t, map[string]string{
"test.txt": "content",
}),
Type: &proto.AcquiredJob_WorkspaceBuild_{
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
Metadata: &sdkproto.Provision_Metadata{},
},
},
}, nil
},
updateJob: func(ctx context.Context, update *proto.UpdateJobRequest) (*proto.UpdateJobResponse, error) {
return &proto.UpdateJobResponse{}, nil
},
failJob: func(ctx context.Context, job *proto.FailedJob) (*proto.Empty, error) {
if second.Load() {
close(completeChan)
return &proto.Empty{}, nil
}
close(failChan)
<-failedChan
return &proto.Empty{}, nil
},
})
if !second.Load() {
go func() {
<-failChan
_ = client.DRPCConn().Close()
second.Store(true)
close(failedChan)
}()
}
return client, nil
}, provisionerd.Provisioners{
"someprovisioner": createProvisionerClient(t, provisionerTestServer{
provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error {
// Ignore the first provision message!
_, _ = stream.Recv()
return stream.Send(&sdkproto.Provision_Response{
Type: &sdkproto.Provision_Response_Complete{
Complete: &sdkproto.Provision_Complete{
Error: "some error",
},
},
})
},
}),
})
<-completeChan
require.NoError(t, server.Close())
})
t.Run("ReconnectAndComplete", func(t *testing.T) {
t.Parallel()
var second atomic.Bool
failChan := make(chan struct{})
failedChan := make(chan struct{})
completeChan := make(chan struct{})
server := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
client := createProvisionerDaemonClient(t, provisionerDaemonTestServer{
acquireJob: func(ctx context.Context, _ *proto.Empty) (*proto.AcquiredJob, error) {
if second.Load() {
close(completeChan)
return &proto.AcquiredJob{}, nil
}
return &proto.AcquiredJob{
JobId: "test",
Provisioner: "someprovisioner",
TemplateSourceArchive: createTar(t, map[string]string{
"test.txt": "content",
}),
Type: &proto.AcquiredJob_WorkspaceBuild_{
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
Metadata: &sdkproto.Provision_Metadata{},
},
},
}, nil
},
failJob: func(ctx context.Context, job *proto.FailedJob) (*proto.Empty, error) {
return nil, yamux.ErrSessionShutdown
},
updateJob: func(ctx context.Context, update *proto.UpdateJobRequest) (*proto.UpdateJobResponse, error) {
return &proto.UpdateJobResponse{}, nil
},
completeJob: func(ctx context.Context, job *proto.CompletedJob) (*proto.Empty, error) {
if second.Load() {
return &proto.Empty{}, nil
}
close(failChan)
<-failedChan
return &proto.Empty{}, nil
},
})
if !second.Load() {
go func() {
<-failChan
_ = client.DRPCConn().Close()
second.Store(true)
close(failedChan)
}()
}
return client, nil
}, provisionerd.Provisioners{
"someprovisioner": createProvisionerClient(t, provisionerTestServer{
provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error {
// Ignore the first provision message!
_, _ = stream.Recv()
return stream.Send(&sdkproto.Provision_Response{
Type: &sdkproto.Provision_Response_Complete{
Complete: &sdkproto.Provision_Complete{},
},
})
},
}),
})
<-completeChan
require.NoError(t, server.Close())
})
}
// Creates an in-memory tar of the files provided.
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v3.19.4
// protoc v3.20.0
// source: provisionersdk/proto/provisioner.proto
package proto
+6
View File
@@ -9,6 +9,12 @@ import (
"storj.io/drpc/drpcconn"
)
const (
// MaxMessageSize is the maximum payload size that can be
// transported without error.
MaxMessageSize = 4 << 20
)
// TransportPipe creates an in-memory pipe for dRPC transport.
func TransportPipe() (*yamux.Session, *yamux.Session) {
clientReader, clientWriter := io.Pipe()
+1 -1
View File
@@ -183,7 +183,7 @@ func toTsType(fieldType string) string {
switch fieldType {
case "bool":
return "boolean"
case "uint64", "uint32", "float64":
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", "float32", "float64":
return "number"
}
+1 -1
View File
@@ -17,7 +17,7 @@ const config: PlaywrightTestConfig = {
// https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests
webServer: {
// Run the coder daemon directly.
command: `go run -tags embed ${path.join(__dirname, "../../cmd/coder/main.go")} start --dev --skip-tunnel`,
command: `go run -tags embed ${path.join(__dirname, "../../cmd/coder/main.go")} server --dev --skip-tunnel`,
port: 3000,
timeout: 120 * 10000,
reuseExistingServer: false,
+8 -3
View File
@@ -2,7 +2,7 @@ import axios, { AxiosRequestHeaders } from "axios"
import { mutate } from "swr"
import { MockPager, MockUser, MockUser2 } from "../testHelpers/entities"
import * as Types from "./types"
import * as GenTypes from "./typesGenerated"
import * as TypesGen from "./typesGenerated"
const CONTENT_TYPE_JSON: AxiosRequestHeaders = {
"Content-Type": "application/json",
@@ -66,6 +66,11 @@ export const getUser = async (): Promise<Types.UserResponse> => {
return response.data
}
export const getAuthMethods = async (): Promise<TypesGen.AuthMethods> => {
const response = await axios.get<TypesGen.AuthMethods>("/api/v2/users/authmethods")
return response.data
}
export const getApiKey = async (): Promise<Types.APIKeyResponse> => {
const response = await axios.post<Types.APIKeyResponse>("/api/v2/users/me/keys")
return response.data
@@ -80,8 +85,8 @@ export const getUsers = async (): Promise<Types.PagedUsers> => {
})
}
export const createUser = async (user: GenTypes.CreateUserRequest): Promise<GenTypes.User> => {
const response = await axios.post<GenTypes.User>("/api/v2/users/", user)
export const createUser = async (user: TypesGen.CreateUserRequest): Promise<TypesGen.User> => {
const response = await axios.post<TypesGen.User>("/api/v2/users/", user)
return response.data
}
+24 -12
View File
@@ -16,6 +16,7 @@ export interface GitSSHKey {
// From codersdk/gitsshkey.go:21:6.
export interface AgentGitSSHKey {
readonly public_key: string
readonly private_key: string
}
@@ -46,13 +47,13 @@ export interface CreateParameterRequest {
readonly source_value: string
}
// From codersdk/provisionerdaemons.go:37:6.
// From codersdk/provisionerdaemons.go:46:6.
export interface ProvisionerJob {
readonly error: string
readonly status: ProvisionerJobStatus
}
// From codersdk/provisionerdaemons.go:47:6.
// From codersdk/provisionerdaemons.go:56:6.
export interface ProvisionerJobLog {
readonly stage: string
readonly output: string
@@ -71,13 +72,19 @@ export interface TemplateVersion {
}
// From codersdk/users.go:17:6.
export interface UsersRequest {
readonly search: string
readonly limit: number
readonly offset: number
}
// From codersdk/users.go:32:6.
export interface User {
readonly email: string
readonly username: string
readonly name: string
}
// From codersdk/users.go:25:6.
// From codersdk/users.go:39:6.
export interface CreateFirstUserRequest {
readonly email: string
readonly username: string
@@ -85,46 +92,51 @@ export interface CreateFirstUserRequest {
readonly organization: string
}
// From codersdk/users.go:38:6.
// From codersdk/users.go:52:6.
export interface CreateUserRequest {
readonly email: string
readonly username: string
readonly password: string
}
// From codersdk/users.go:45:6.
// From codersdk/users.go:59:6.
export interface UpdateUserProfileRequest {
readonly email: string
readonly username: string
readonly name?: string
}
// From codersdk/users.go:52:6.
// From codersdk/users.go:65:6.
export interface LoginWithPasswordRequest {
readonly email: string
readonly password: string
}
// From codersdk/users.go:58:6.
// From codersdk/users.go:71:6.
export interface LoginWithPasswordResponse {
readonly session_token: string
}
// From codersdk/users.go:63:6.
// From codersdk/users.go:76:6.
export interface GenerateAPIKeyResponse {
readonly key: string
}
// From codersdk/users.go:67:6.
// From codersdk/users.go:80:6.
export interface CreateOrganizationRequest {
readonly name: string
}
// From codersdk/users.go:72:6.
// From codersdk/users.go:85:6.
export interface CreateWorkspaceRequest {
readonly name: string
}
// From codersdk/users.go:94:6.
export interface AuthMethods {
readonly password: boolean
readonly github: boolean
}
// From codersdk/workspaceagents.go:31:6.
export interface GoogleInstanceIdentityToken {
readonly json_web_token: string
@@ -24,7 +24,26 @@ SignedOut.args = {
}
export const Loading = Template.bind({})
Loading.args = { ...SignedOut.args, isLoading: true }
Loading.args = {
...SignedOut.args,
isLoading: true,
authMethods: {
github: true,
password: true,
},
}
export const WithError = Template.bind({})
WithError.args = { ...SignedOut.args, authErrorMessage: "Email or password was invalid" }
export const WithLoginError = Template.bind({})
WithLoginError.args = { ...SignedOut.args, authErrorMessage: "Email or password was invalid" }
export const WithAuthMethodsError = Template.bind({})
WithAuthMethodsError.args = { ...SignedOut.args, methodsErrorMessage: "Failed to fetch auth methods" }
export const WithGithub = Template.bind({})
WithGithub.args = {
...SignedOut.args,
authMethods: {
password: true,
github: true,
},
}
+27 -3
View File
@@ -1,9 +1,12 @@
import Button from "@material-ui/core/Button"
import FormHelperText from "@material-ui/core/FormHelperText"
import Link from "@material-ui/core/Link"
import { makeStyles } from "@material-ui/core/styles"
import TextField from "@material-ui/core/TextField"
import { FormikContextType, useFormik } from "formik"
import React from "react"
import * as Yup from "yup"
import { AuthMethods } from "../../api/typesGenerated"
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
import { Welcome } from "../Welcome/Welcome"
import { LoadingButton } from "./../LoadingButton/LoadingButton"
@@ -24,7 +27,9 @@ export const Language = {
emailInvalid: "Please enter a valid email address.",
emailRequired: "Please enter an email address.",
authErrorMessage: "Incorrect email or password.",
signIn: "Sign In",
methodsErrorMessage: "Unable to fetch auth methods.",
passwordSignIn: "Sign In",
githubSignIn: "GitHub",
}
const validationSchema = Yup.object({
@@ -49,10 +54,18 @@ const useStyles = makeStyles((theme) => ({
export interface SignInFormProps {
isLoading: boolean
authErrorMessage?: string
methodsErrorMessage?: string
authMethods?: AuthMethods
onSubmit: ({ email, password }: { email: string; password: string }) => Promise<void>
}
export const SignInForm: React.FC<SignInFormProps> = ({ isLoading, authErrorMessage, onSubmit }) => {
export const SignInForm: React.FC<SignInFormProps> = ({
authMethods,
isLoading,
authErrorMessage,
methodsErrorMessage,
onSubmit,
}) => {
const styles = useStyles()
const form: FormikContextType<BuiltInAuthFormValues> = useFormik<BuiltInAuthFormValues>({
@@ -76,6 +89,7 @@ export const SignInForm: React.FC<SignInFormProps> = ({ isLoading, authErrorMess
className={styles.loginTextField}
fullWidth
label={Language.emailLabel}
type="email"
variant="outlined"
/>
<TextField
@@ -89,12 +103,22 @@ export const SignInForm: React.FC<SignInFormProps> = ({ isLoading, authErrorMess
variant="outlined"
/>
{authErrorMessage && <FormHelperText error>{Language.authErrorMessage}</FormHelperText>}
{methodsErrorMessage && <FormHelperText error>{Language.methodsErrorMessage}</FormHelperText>}
<div className={styles.submitBtn}>
<LoadingButton color="primary" loading={isLoading} fullWidth type="submit" variant="contained">
{isLoading ? "" : Language.signIn}
{isLoading ? "" : Language.passwordSignIn}
</LoadingButton>
</div>
</form>
{authMethods?.github && (
<div className={styles.submitBtn}>
<Link href="/api/v2/users/oauth2/github/callback">
<Button color="primary" disabled={isLoading} fullWidth type="submit" variant="contained">
{Language.githubSignIn}
</Button>
</Link>
</div>
)}
</>
)
}
+41 -2
View File
@@ -23,7 +23,7 @@ describe("LoginPage", () => {
render(<LoginPage />)
// Then
await screen.findByText(Language.signIn)
await screen.findByText(Language.passwordSignIn)
})
it("shows an error message if SignIn fails", async () => {
@@ -42,7 +42,7 @@ describe("LoginPage", () => {
await userEvent.type(email, "test@coder.com")
await userEvent.type(password, "password")
// Click sign-in
const signInButton = await screen.findByText(Language.signIn)
const signInButton = await screen.findByText(Language.passwordSignIn)
act(() => signInButton.click())
// Then
@@ -50,4 +50,43 @@ describe("LoginPage", () => {
expect(errorMessage).toBeDefined()
expect(history.location.pathname).toEqual("/login")
})
it("shows an error if fetching auth methods fails", async () => {
// Given
server.use(
// Make login fail
rest.get("/api/v2/users/authmethods", async (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ message: "nope" }))
}),
)
// When
render(<LoginPage />)
// Then
const errorMessage = await screen.findByText(Language.methodsErrorMessage)
expect(errorMessage).toBeDefined()
})
it("shows github authentication when enabled", async () => {
// Given
server.use(
rest.get("/api/v2/users/authmethods", async (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
password: true,
github: true,
}),
)
}),
)
// When
render(<LoginPage />)
// Then
await screen.findByText(Language.passwordSignIn)
await screen.findByText(Language.githubSignIn)
})
})
+10 -1
View File
@@ -35,6 +35,9 @@ export const LoginPage: React.FC = () => {
const isLoading = authState.hasTag("loading")
const redirectTo = retrieveRedirect(location.search)
const authErrorMessage = authState.context.authError ? (authState.context.authError as Error).message : undefined
const getMethodsError = authState.context.getMethodsError
? (authState.context.getMethodsError as Error).message
: undefined
const onSubmit = async ({ email, password }: { email: string; password: string }) => {
authSend({ type: "SIGN_IN", email, password })
@@ -47,7 +50,13 @@ export const LoginPage: React.FC = () => {
<div className={styles.root}>
<div className={styles.layout}>
<div className={styles.container}>
<SignInForm isLoading={isLoading} authErrorMessage={authErrorMessage} onSubmit={onSubmit} />
<SignInForm
authMethods={authState.context.methods}
isLoading={isLoading}
authErrorMessage={authErrorMessage}
methodsErrorMessage={getMethodsError}
onSubmit={onSubmit}
/>
</div>
<Footer />
+6
View File
@@ -9,6 +9,7 @@ import {
Workspace,
WorkspaceAutostartRequest,
} from "../api/types"
import { AuthMethods } from "../api/typesGenerated"
export const MockSessionToken = { session_token: "my-session-token" }
@@ -97,3 +98,8 @@ export const MockUserAgent: UserAgent = {
ip_address: "11.22.33.44",
os: "Windows 10",
}
export const MockAuthMethods: AuthMethods = {
password: true,
github: false,
}
+3
View File
@@ -42,6 +42,9 @@ export const handlers = [
rest.get("/api/v2/users/me/keys", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockAPIKey))
}),
rest.get("/api/v2/users/authmethods", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockAuthMethods))
}),
// workspaces
rest.get("/api/v2/workspaces/:workspaceId", async (req, res, ctx) => {
+47 -3
View File
@@ -1,16 +1,20 @@
import { assign, createMachine } from "xstate"
import * as API from "../../api"
import * as Types from "../../api/types"
import * as TypesGen from "../../api/typesGenerated"
import { displaySuccess } from "../../components/GlobalSnackbar/utils"
export const Language = {
successProfileUpdate: "Updated preferences.",
}
export interface AuthContext {
getUserError?: Error | unknown
getMethodsError?: Error | unknown
authError?: Error | unknown
updateProfileError?: Error | unknown
me?: Types.UserResponse
methods?: TypesGen.AuthMethods
}
export type AuthEvent =
@@ -19,10 +23,17 @@ export type AuthEvent =
| { type: "UPDATE_PROFILE"; data: Types.UpdateProfileRequest }
export const authMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QEMCuAXAFgZXc9YAdLAJZQB2kA8hgMTYCSA4gHID6DLioADgPal0JPuW4gAHogCMABgCcAFkLyZAVilS5GgEwAOAMwA2ADQgAnogDsa5YcOXD2y7oWH92-QF9PptFlz4RKQUJORQDOS0ECJEoQBufADWQWTkEWL8gsKiSBKI7kraqpZSuh7aUvpy6vqmFghlcoT6qgb6GsUlctrevhg4eATEqaHhkWAAThN8E4Q8ADb4AGYzALbDFOm5mSRCImKSCLKKynJqGlpSekZ1iIaltvaOzq4FvSB+A4GEMOhCYQBVWCTKIxQjxJJEX4AWTAGQEu2yB0QrikhG0Mn0+l0ulUWJkhlatXMKKqynkCn0lOscnsUnenwCQ1+-ygQJBk2mswWyzWPzA6Fh8Ky+1yh202iacip2I8hjU9m0twQ1lUzSk9hkHlUCjU2kMDP6TJSFEgETm0yWJHmsQgNtoAIACgARACCABUAKJsR0AJSoADEGAAZT3CxGi0CHGTKmSG-yDE2UCDmniW61EVA8CD4UaO9P26KUcHkBLJQiMxMbZOpguZ7O5sL5vhWm0ICEAY1zIgA2jIALrhvY5KPSKSWNWKWQeaUKSXj5UY3SEUrdKqExy08fxr5DYI18gWlsZwhZnOs5utsC0TkzOaLdArCbrSvffdmw9p48208Ni919tSz4Lthz7QdtgRYdkQaLRCF0SxtAUcdpQnRxdGVXV9EIKdFBkGRdDkSxFGKHdjWrD96GYdgqABd0hyRMVpCxdFaSxVQZEsIxx0sSxlUI9Eql1DUJQnRQ5FIqt91GGh0FBYsIXLfcZPoyM8gQdwsIQuRdEMSlSm1DxlR1Zc1AIhDdJwrwfA+I1JJGMIZJvKY7x5R8+SUjAVJHNSijVdwCIUdQriJfReJJBAihkZpLAUJDikigwJW8azyD4CA4DEV891SahPIgkVvMOdRDEINxqTUbFCW05UJywhQDFUNd2gxQxxOsrKk1GLZeEghjRyOfUoqkPEOOlQj2mVVrtDgjUEPYkpcT0CTvhZUZ2QmLzoLnZVdA1OCiS0TjcU0VRluy00U0-OtwTtIhUs9ZyNvyiNCrHHi4InPCFE4wktQwvbQtiloWu0jFLDOpMPyPK8bp-W8np6groI0d74PYmRvqMdilXC1wSvYxRYsIwbrHB9rbLfHLLuhk8SFuzbGKOYasLRr6fux5UZWi2KCclXarL6BNKYu2tv3rc88zrBn+pxTTGsqULp2xKRlRO0qCJnDdJXuMnBd3SHqa-K9pbUgBaRDlVNzjyTwjpOM+1QDXJoXzoPE3DlN+rLakVwba1dpvvYjQIeraS8sRl7oPcQgicQicihxGQfaM9jlFaWdSgJDR6Wd-X3cQZdY8DhPdCThRLY8dUOPsBwDE4wjdGSzwgA */
/** @xstate-layout N4IgpgJg5mDOIC5QEMCuAXAFgZXc9YAdLAJZQB2kA8hgMTYCSA4gHID6DLioADgPal0JPuW4gAHogDsABgCshOQA4AzABY5ARgBMUgJxrtcgDQgAnok0zNSwkpkrNKvQDY3aqS6kBfb6bRYuPhEpBQk5FAM5LQQIkThAG58ANYhZORRYvyCwqJIEohyMlKE2mrFKo7aLq5SJuaILtq2mjZqrboy2s4+fiABOHgExOnhkdFgAE6TfJOEPAA2+ABmswC2IxSZ+dkkQiJikgiyCsrqWroGRqYWCHoq2oQyDpouXa1qGmq+-hiDwYQYOghBEAKqwKYxOKERIpIhAgCyYCyAj2uUOiF0mieTik3UqMhqSleN0QhgUOhUbzUKhk9jKch+-T+QWGQJBUHBkKmMzmixW60BYHQSJROQO+SOUmxVKUniUcjkUgVLjlpOOCqecj0LyKmmVeiUTIGrPhwo5SKwfAgsChlBh5CSqSFIuFmGt8B2qP2eVARxUeNKCo02k0cllTRc6qUalsCtU731DikvV+gSGZuBY0t7pttB5s3mS3Qq0mG0Rbo9YrREr9iHUMkIUnKHSklQVKnqtz0BkImjUbmeShcVmejL6Jozm0oECi8xmyxIC3iEGXtFBAAUACIAQQAKgBRNgbgBKVAAYgwADIH6s+jEIOV6Qgj1oxnTBkfq7qNlxyT4aC4saaPcVLGiyU6hDOc48AuS5EKgPAQPgYwbnBa6xPasLOpOAJQZAMHoQhSEoREaF8Iuy4ILCADGKEiAA2jIAC6d7opKiBPi+rRtB+-5fg0CDqM+YafG8+jWLI2jgemeHpAR5DzhR8GEIhyEcuRlFgPm0yFvyJaCrhwz4bOimwcpy6qSRGlEdRjp8HRPpMaxXrir6BSPvo3Fvu0zT8Zo6oDmoTb-p8cjaFcsjqDJ-zGfJpn0Mw7BUKCe5sbWHm6CU2jWKGnhaFSeLqv2jZKMOehOE49i0v+MWmtOYw0OgdrxPZzpQU16XuUcAC0-aPGGSgVQOTS4ko2jfjGhC0gB1WeDIBguHVkGjBETU6byRYCmW06da5NbdZiKalLl+p-k4XgTYJNhxuVNKGCB77fEy5DWnAYhGWkFDUBgXUPoqLiKOoxIqGVejNKoxXPCUMgjZ8zj6sORoThBclhBE2y8N67F1o+xIvhoejavqEX-gFgl6IGFWOC2bzWNqy0AuyYxcpMf0cQghjqn+JT6GJeJynIqrI2msWZhalY2uzuN9dojwpn+epDvqHjRkUL7KBDEljiLzKyXF32mUpWkwquRCvQeuls-t94c606s8c0A5C00UjqqDja5cUXQRXiDiMwb0FmURpuWQW1tY25D7242jsxn+bi6IFhh2K4qjKP+fsqAHX1B8bKkkGb0seR0gNx87idu4JtIwzoxTdELzbheOov1SZhEWcR6moURxdHCOgPhsBoNDRDKju84hCfHS4ZAXPqg59OCn58ufeFODQPD2DY-FUqGsASmdShgvKP67nClrwgQu2EPIPb2V4+CVNf7w5TOphs22en2LDVrb9Ns4w8j1coU8iafDKJVWQagDDqmOsOKBVhXDgweNJb+ppL59TcH2ZQw1E5jSurcYK8CHCODfE0B4vRfBAA */
createMachine(
{
context: { me: undefined, getUserError: undefined, authError: undefined, updateProfileError: undefined },
context: {
me: undefined,
getUserError: undefined,
authError: undefined,
updateProfileError: undefined,
methods: undefined,
getMethodsError: undefined,
},
tsTypes: {} as import("./authXService.typegen").Typegen0,
schema: {
context: {} as AuthContext,
@@ -31,6 +42,9 @@ export const authMachine =
getMe: {
data: Types.UserResponse
}
getMethods: {
data: TypesGen.AuthMethods
}
signIn: {
data: Types.LoginResponse
}
@@ -81,6 +95,25 @@ export const authMachine =
onError: [
{
actions: "assignGetUserError",
target: "gettingMethods",
},
],
},
tags: "loading",
},
gettingMethods: {
invoke: {
src: "getMethods",
id: "getMethods",
onDone: [
{
actions: ["assignMethods", "clearGetMethodsError"],
target: "signedOut",
},
],
onError: [
{
actions: "assignGetMethodsError",
target: "signedOut",
},
],
@@ -139,7 +172,7 @@ export const authMachine =
onDone: [
{
actions: ["unassignMe", "clearAuthError"],
target: "signedOut",
target: "gettingMethods",
},
],
onError: [
@@ -160,6 +193,7 @@ export const authMachine =
},
signOut: API.logout,
getMe: API.getUser,
getMethods: API.getAuthMethods,
updateProfile: async (context, event) => {
if (!context.me) {
throw new Error("No current user found")
@@ -176,6 +210,16 @@ export const authMachine =
...context,
me: undefined,
})),
assignMethods: assign({
methods: (_, event) => event.data,
}),
assignGetMethodsError: assign({
getMethodsError: (_, event) => event.data,
}),
clearGetMethodsError: assign((context: AuthContext) => ({
...context,
getMethodsError: undefined,
})),
assignGetUserError: assign({
getUserError: (_, event) => event.data,
}),