mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Merge branch 'main' into create-user/presleyp/734
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -35,3 +35,5 @@ site/out/
|
||||
*.tfplan
|
||||
*.lock.hcl
|
||||
.terraform/
|
||||
|
||||
.vscode/*.log
|
||||
|
||||
Vendored
+2
@@ -35,6 +35,7 @@
|
||||
"nolint",
|
||||
"nosec",
|
||||
"ntqry",
|
||||
"OIDC",
|
||||
"oneof",
|
||||
"parameterscopeid",
|
||||
"pqtype",
|
||||
@@ -46,6 +47,7 @@
|
||||
"ptytest",
|
||||
"retrier",
|
||||
"sdkproto",
|
||||
"Signup",
|
||||
"stretchr",
|
||||
"TCGETS",
|
||||
"tcpip",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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{})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -6,5 +6,6 @@ func users() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "users",
|
||||
}
|
||||
cmd.AddCommand(userCreate(), userList())
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Generated
+9
-21
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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{
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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, ¶ms) {
|
||||
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
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
@@ -19,6 +19,7 @@ type GitSSHKey struct {
|
||||
}
|
||||
|
||||
type AgentGitSSHKey struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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`:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
Generated
+1
-1
@@ -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
|
||||
|
||||
Generated
+1
-1
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Generated
+1
-1
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user