mirror of https://github.com/coder/coder.git
feat: Add templates to create working release (#422)
* Add templates
* Move API structs to codersdk
* Back to green tests!
* It all works, but now with tea! 🧋
* It works!
* Add cancellation to provisionerd
* Tests pass!
* Add deletion of workspaces and projects
* Fix agent lock
* Add clog
* Fix linting errors
* Remove unused CLI tests
* Rename daemon to start
* Fix leaking command
* Fix promptui test
* Update agent connection frequency
* Skip login tests on Windows
* Increase tunnel connect timeout
* Fix templater
* Lower test requirements
* Fix embed
* Disable promptui tests for Windows
* Fix write newline
* Fix PTY write newline
* Fix CloseReader
* Fix compilation on Windows
* Fix linting error
* Remove bubbletea
* Cleanup readwriter
* Use embedded templates instead of serving over API
* Move templates to examples
* Improve workspace create flow
* Fix Windows build
* Fix tests
* Fix linting errors
* Fix untar with extracting max size
* Fix newline char
This commit is contained in:
parent
2818b3ce6d
commit
c451f4e685
|
@ -157,7 +157,7 @@ jobs:
|
|||
- name: Test with Mock Database
|
||||
shell: bash
|
||||
env:
|
||||
GOCOUNT: ${{ runner.os == 'Windows' && 3 || 5 }}
|
||||
GOCOUNT: ${{ runner.os == 'Windows' && 1 || 2 }}
|
||||
GOMAXPROCS: ${{ runner.os == 'Windows' && 1 || 2 }}
|
||||
run: gotestsum --junitfile="gotests.xml" --packages="./..." --
|
||||
-covermode=atomic -coverprofile="gotests.coverage"
|
||||
|
@ -178,7 +178,7 @@ jobs:
|
|||
run: DB=true gotestsum --junitfile="gotests.xml" --packages="./..." --
|
||||
-covermode=atomic -coverprofile="gotests.coverage" -timeout=3m
|
||||
-coverpkg=./...,github.com/coder/coder/codersdk
|
||||
-count=1 -race -parallel=2 -failfast
|
||||
-count=1 -parallel=2 -failfast
|
||||
|
||||
- name: Upload DataDog Trace
|
||||
if: (success() || failure()) && github.actor != 'dependabot[bot]' && runner.os == 'Linux'
|
||||
|
|
|
@ -28,3 +28,8 @@ site/**/*.typegen.ts
|
|||
# Build
|
||||
dist/
|
||||
site/out/
|
||||
|
||||
*.tfstate
|
||||
*.tfplan
|
||||
*.lock.hcl
|
||||
.terraform/
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
archives:
|
||||
- builds:
|
||||
- id: coder
|
||||
builds:
|
||||
- coder
|
||||
files:
|
||||
- README.md
|
||||
|
@ -12,7 +13,6 @@ before:
|
|||
builds:
|
||||
- id: coder-slim
|
||||
dir: cmd/coder
|
||||
flags: [-tags=slim]
|
||||
ldflags: ["-s -w"]
|
||||
env: [CGO_ENABLED=0]
|
||||
goos: [darwin, linux, windows]
|
||||
|
@ -20,10 +20,11 @@ builds:
|
|||
hooks:
|
||||
# The "trimprefix" appends ".exe" on Windows.
|
||||
post: |
|
||||
cp {{.Path}} site/out/bin/coder_{{ .Os }}_{{ .Arch }}{{ trimprefix .Name "coder" }}
|
||||
cp {{.Path}} site/out/bin/coder-{{ .Os }}-{{ .Arch }}{{ trimprefix .Name "coder" }}
|
||||
|
||||
- id: coder
|
||||
dir: cmd/coder
|
||||
flags: [-tags=embed]
|
||||
ldflags: ["-s -w"]
|
||||
env: [CGO_ENABLED=0]
|
||||
goos: [darwin, linux, windows]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"cliui",
|
||||
"coderd",
|
||||
"coderdtest",
|
||||
"codersdk",
|
||||
|
@ -12,8 +13,10 @@
|
|||
"goleak",
|
||||
"gossh",
|
||||
"hashicorp",
|
||||
"hclsyntax",
|
||||
"httpmw",
|
||||
"idtoken",
|
||||
"Iflag",
|
||||
"incpatch",
|
||||
"isatty",
|
||||
"Jobf",
|
||||
|
@ -40,11 +43,14 @@
|
|||
"retrier",
|
||||
"sdkproto",
|
||||
"stretchr",
|
||||
"TCGETS",
|
||||
"tcpip",
|
||||
"TCSETS",
|
||||
"tfexec",
|
||||
"tfstate",
|
||||
"trimprefix",
|
||||
"unconvert",
|
||||
"Untar",
|
||||
"webrtc",
|
||||
"xerrors",
|
||||
"yamux"
|
||||
|
@ -69,7 +75,7 @@
|
|||
"go.coverOnSave": true,
|
||||
// The codersdk is used by coderd another other packages extensively.
|
||||
// To reduce redundancy in tests, it's covered by other packages.
|
||||
"go.testFlags": ["-coverpkg=./.,github.com/coder/coder/codersdk"],
|
||||
"go.testFlags": ["-short", "-coverpkg=./.,github.com/coder/coder/codersdk"],
|
||||
"go.coverageDecorator": {
|
||||
"type": "gutter",
|
||||
"coveredHighlightColor": "rgba(64,128,128,0.5)",
|
||||
|
|
8
Makefile
8
Makefile
|
@ -55,6 +55,10 @@ install: bin
|
|||
@echo "-- CLI available at $(shell ls $(INSTALL_DIR)/coder*)"
|
||||
.PHONY: install
|
||||
|
||||
package:
|
||||
goreleaser release --snapshot --rm-dist
|
||||
.PHONY: package
|
||||
|
||||
peerbroker/proto: peerbroker/proto/peerbroker.proto
|
||||
protoc \
|
||||
--go_out=. \
|
||||
|
@ -89,7 +93,3 @@ site/out:
|
|||
# Restores GITKEEP files!
|
||||
git checkout HEAD site/out
|
||||
.PHONY: site/out
|
||||
|
||||
snapshot:
|
||||
goreleaser release --snapshot --rm-dist
|
||||
.PHONY: snapshot
|
||||
|
|
143
agent/agent.go
143
agent/agent.go
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"sync"
|
||||
|
@ -85,6 +86,76 @@ type server struct {
|
|||
sshServer *ssh.Server
|
||||
}
|
||||
|
||||
func (s *server) run(ctx context.Context) {
|
||||
var peerListener *peerbroker.Listener
|
||||
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); {
|
||||
peerListener, err = s.clientDialer(ctx, s.options)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
if s.isClosed() {
|
||||
return
|
||||
}
|
||||
s.options.Logger.Warn(context.Background(), "failed to dial", slog.Error(err))
|
||||
continue
|
||||
}
|
||||
s.options.Logger.Debug(context.Background(), "connected")
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
for {
|
||||
conn, err := peerListener.Accept()
|
||||
if err != nil {
|
||||
if s.isClosed() {
|
||||
return
|
||||
}
|
||||
s.options.Logger.Debug(ctx, "peer listener accept exited; restarting connection", slog.Error(err))
|
||||
s.run(ctx)
|
||||
return
|
||||
}
|
||||
s.closeMutex.Lock()
|
||||
s.connCloseWait.Add(1)
|
||||
s.closeMutex.Unlock()
|
||||
go s.handlePeerConn(ctx, conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) handlePeerConn(ctx context.Context, conn *peer.Conn) {
|
||||
go func() {
|
||||
<-conn.Closed()
|
||||
s.connCloseWait.Done()
|
||||
}()
|
||||
for {
|
||||
channel, err := conn.Accept(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, peer.ErrClosed) || s.isClosed() {
|
||||
return
|
||||
}
|
||||
s.options.Logger.Debug(ctx, "accept channel from peer connection", slog.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
switch channel.Protocol() {
|
||||
case "ssh":
|
||||
s.sshServer.HandleConn(channel.NetConn())
|
||||
default:
|
||||
s.options.Logger.Warn(ctx, "unhandled protocol from channel",
|
||||
slog.F("protocol", channel.Protocol()),
|
||||
slog.F("label", channel.Label()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) init(ctx context.Context) {
|
||||
// Clients' should ignore the host key when connecting.
|
||||
// The agent needs to authenticate with coderd to SSH,
|
||||
|
@ -197,7 +268,7 @@ func (*server) handleSSHSession(session ssh.Session) error {
|
|||
}()
|
||||
|
||||
cmd := exec.CommandContext(session.Context(), command, args...)
|
||||
cmd.Env = session.Environ()
|
||||
cmd.Env = append(os.Environ(), session.Environ()...)
|
||||
|
||||
sshPty, windowSize, isPty := session.Pty()
|
||||
if isPty {
|
||||
|
@ -244,76 +315,6 @@ func (*server) handleSSHSession(session ssh.Session) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *server) run(ctx context.Context) {
|
||||
var peerListener *peerbroker.Listener
|
||||
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); {
|
||||
peerListener, err = s.clientDialer(ctx, s.options)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
if s.isClosed() {
|
||||
return
|
||||
}
|
||||
s.options.Logger.Warn(context.Background(), "failed to dial", slog.Error(err))
|
||||
continue
|
||||
}
|
||||
s.options.Logger.Debug(context.Background(), "connected")
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
for {
|
||||
conn, err := peerListener.Accept()
|
||||
if err != nil {
|
||||
if s.isClosed() {
|
||||
return
|
||||
}
|
||||
s.options.Logger.Debug(ctx, "peer listener accept exited; restarting connection", slog.Error(err))
|
||||
s.run(ctx)
|
||||
return
|
||||
}
|
||||
s.closeMutex.Lock()
|
||||
s.connCloseWait.Add(1)
|
||||
s.closeMutex.Unlock()
|
||||
go s.handlePeerConn(ctx, conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) handlePeerConn(ctx context.Context, conn *peer.Conn) {
|
||||
go func() {
|
||||
<-conn.Closed()
|
||||
s.connCloseWait.Done()
|
||||
}()
|
||||
for {
|
||||
channel, err := conn.Accept(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, peer.ErrClosed) || s.isClosed() {
|
||||
return
|
||||
}
|
||||
s.options.Logger.Debug(ctx, "accept channel from peer connection", slog.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
switch channel.Protocol() {
|
||||
case "ssh":
|
||||
s.sshServer.HandleConn(channel.NetConn())
|
||||
default:
|
||||
s.options.Logger.Warn(ctx, "unhandled protocol from channel",
|
||||
slog.F("protocol", channel.Protocol()),
|
||||
slog.F("label", channel.Label()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isClosed returns whether the API is closed or not.
|
||||
func (s *server) isClosed() bool {
|
||||
select {
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
package usershell
|
||||
|
||||
import "os/exec"
|
||||
|
||||
// Get returns the command prompt binary name.
|
||||
func Get(username string) (string, error) {
|
||||
_, err := exec.LookPath("powershell.exe")
|
||||
if err == nil {
|
||||
return "powershell.exe", nil
|
||||
}
|
||||
return "cmd.exe", nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package cliui
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/charm/ui/common"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
Canceled = xerrors.New("canceled")
|
||||
|
||||
defaultStyles = common.DefaultStyles()
|
||||
)
|
||||
|
||||
// ValidateNotEmpty is a helper function to disallow empty inputs!
|
||||
func ValidateNotEmpty(s string) error {
|
||||
if s == "" {
|
||||
return xerrors.New("Must be provided!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Styles compose visual elements of the UI!
|
||||
var Styles = struct {
|
||||
Bold,
|
||||
Code,
|
||||
Field,
|
||||
Keyword,
|
||||
Paragraph,
|
||||
Placeholder,
|
||||
Prompt,
|
||||
FocusedPrompt,
|
||||
Fuschia,
|
||||
Logo,
|
||||
Warn,
|
||||
Wrap lipgloss.Style
|
||||
}{
|
||||
Bold: lipgloss.NewStyle().Bold(true),
|
||||
Code: defaultStyles.Code,
|
||||
Field: defaultStyles.Code.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FFFFFF"}),
|
||||
Keyword: defaultStyles.Keyword,
|
||||
Paragraph: defaultStyles.Paragraph,
|
||||
Placeholder: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
|
||||
Prompt: defaultStyles.Prompt.Foreground(lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"}),
|
||||
FocusedPrompt: defaultStyles.FocusedPrompt.Foreground(lipgloss.Color("#651fff")),
|
||||
Fuschia: defaultStyles.SelectedMenuItem.Copy(),
|
||||
Logo: defaultStyles.Logo.SetString("Coder"),
|
||||
Warn: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"}),
|
||||
Wrap: defaultStyles.Wrap,
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
package cliui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
type JobOptions struct {
|
||||
Title string
|
||||
Fetch func() (codersdk.ProvisionerJob, error)
|
||||
Cancel func() error
|
||||
Logs func() (<-chan codersdk.ProvisionerJobLog, error)
|
||||
}
|
||||
|
||||
// Job renders a provisioner job.
|
||||
func Job(cmd *cobra.Command, opts JobOptions) (codersdk.ProvisionerJob, error) {
|
||||
var (
|
||||
spin = spinner.New(spinner.CharSets[5], 100*time.Millisecond, spinner.WithColor("fgGreen"))
|
||||
|
||||
started = false
|
||||
completed = false
|
||||
job codersdk.ProvisionerJob
|
||||
)
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s%s %s\n", Styles.FocusedPrompt, opts.Title, Styles.Placeholder.Render("(ctrl+c to cancel)"))
|
||||
|
||||
spin.Writer = cmd.OutOrStdout()
|
||||
defer spin.Stop()
|
||||
|
||||
// Refreshes the job state!
|
||||
refresh := func() {
|
||||
var err error
|
||||
job, err = opts.Fetch()
|
||||
if err != nil {
|
||||
// If a single fetch fails, it could be a one-off.
|
||||
return
|
||||
}
|
||||
|
||||
if !started && job.StartedAt != nil {
|
||||
spin.Stop()
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), Styles.Prompt.String()+"Started "+Styles.Placeholder.Render("[%dms]")+"\n", job.StartedAt.Sub(job.CreatedAt).Milliseconds())
|
||||
spin.Start()
|
||||
started = true
|
||||
}
|
||||
if !completed && job.CompletedAt != nil {
|
||||
spin.Stop()
|
||||
msg := ""
|
||||
switch job.Status {
|
||||
case codersdk.ProvisionerJobCanceled:
|
||||
msg = "Canceled"
|
||||
case codersdk.ProvisionerJobFailed:
|
||||
msg = "Completed"
|
||||
case codersdk.ProvisionerJobSucceeded:
|
||||
msg = "Built"
|
||||
}
|
||||
started := job.CreatedAt
|
||||
if job.StartedAt != nil {
|
||||
started = *job.StartedAt
|
||||
}
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStderr(), Styles.Prompt.String()+msg+" "+Styles.Placeholder.Render("[%dms]")+"\n", job.CompletedAt.Sub(started).Milliseconds())
|
||||
spin.Start()
|
||||
completed = true
|
||||
}
|
||||
|
||||
switch job.Status {
|
||||
case codersdk.ProvisionerJobPending:
|
||||
spin.Suffix = " Queued"
|
||||
case codersdk.ProvisionerJobRunning:
|
||||
spin.Suffix = " Running"
|
||||
case codersdk.ProvisionerJobCanceling:
|
||||
spin.Suffix = " Canceling"
|
||||
}
|
||||
}
|
||||
refresh()
|
||||
spin.Start()
|
||||
|
||||
stopChan := make(chan os.Signal, 1)
|
||||
defer signal.Stop(stopChan)
|
||||
go func() {
|
||||
signal.Notify(stopChan, os.Interrupt)
|
||||
select {
|
||||
case <-cmd.Context().Done():
|
||||
return
|
||||
case _, ok := <-stopChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
spin.Stop()
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), Styles.FocusedPrompt.String()+"Gracefully canceling... wait for exit or data loss may occur!\n")
|
||||
spin.Start()
|
||||
err := opts.Cancel()
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Failed to cancel %s...\n", err)
|
||||
}
|
||||
refresh()
|
||||
}()
|
||||
|
||||
logs, err := opts.Logs()
|
||||
if err != nil {
|
||||
return job, err
|
||||
}
|
||||
|
||||
firstLog := false
|
||||
ticker := time.NewTicker(time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-cmd.Context().Done():
|
||||
return job, cmd.Context().Err()
|
||||
case <-ticker.C:
|
||||
refresh()
|
||||
if job.CompletedAt != nil {
|
||||
return job, nil
|
||||
}
|
||||
case log, ok := <-logs:
|
||||
if !ok {
|
||||
refresh()
|
||||
continue
|
||||
}
|
||||
if !firstLog {
|
||||
refresh()
|
||||
firstLog = true
|
||||
}
|
||||
spin.Stop()
|
||||
var style lipgloss.Style
|
||||
switch log.Level {
|
||||
case database.LogLevelTrace:
|
||||
style = defaultStyles.Error
|
||||
case database.LogLevelDebug:
|
||||
style = defaultStyles.Error
|
||||
case database.LogLevelError:
|
||||
style = defaultStyles.Error
|
||||
case database.LogLevelWarn:
|
||||
style = Styles.Warn
|
||||
case database.LogLevelInfo:
|
||||
style = defaultStyles.Note
|
||||
}
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s %s %s\n", Styles.Placeholder.Render("|"), style.Render(string(log.Level)), log.Output)
|
||||
spin.Start()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package cliui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coder/coder/coderd/parameter"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func ParameterSchema(cmd *cobra.Command, parameterSchema codersdk.ProjectVersionParameterSchema) (string, error) {
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), Styles.Bold.Render("var."+parameterSchema.Name))
|
||||
if parameterSchema.Description != "" {
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+strings.TrimSpace(strings.Join(strings.Split(parameterSchema.Description, "\n"), "\n "))+"\n")
|
||||
}
|
||||
|
||||
var err error
|
||||
var options []string
|
||||
if parameterSchema.ValidationCondition != "" {
|
||||
options, _, err = parameter.Contains(parameterSchema.ValidationCondition)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
var value string
|
||||
if len(options) > 0 {
|
||||
// Move the cursor up a single line for nicer display!
|
||||
_, _ = fmt.Fprint(cmd.OutOrStdout(), "\033[1A")
|
||||
value, err = Select(cmd, SelectOptions{
|
||||
Options: options,
|
||||
HideSearch: true,
|
||||
})
|
||||
if err == nil {
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout())
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+Styles.Prompt.String()+Styles.Field.Render(value))
|
||||
}
|
||||
} else {
|
||||
value, err = Prompt(cmd, PromptOptions{
|
||||
Text: Styles.Bold.Render("Enter a value:"),
|
||||
})
|
||||
}
|
||||
return value, err
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
package cliui
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
||||
"github.com/bgentry/speakeasy"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// PromptOptions supply a set of options to the prompt.
|
||||
type PromptOptions struct {
|
||||
Text string
|
||||
Default string
|
||||
Secret bool
|
||||
IsConfirm bool
|
||||
Validate func(string) error
|
||||
}
|
||||
|
||||
// Prompt asks the user for input.
|
||||
func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
|
||||
_, _ = fmt.Fprint(cmd.OutOrStdout(), Styles.FocusedPrompt.String()+opts.Text+" ")
|
||||
if opts.IsConfirm {
|
||||
opts.Default = "yes"
|
||||
_, _ = fmt.Fprint(cmd.OutOrStdout(), Styles.Placeholder.Render("("+Styles.Bold.Render("yes")+Styles.Placeholder.Render("/no) ")))
|
||||
} else if opts.Default != "" {
|
||||
_, _ = fmt.Fprint(cmd.OutOrStdout(), Styles.Placeholder.Render("("+opts.Default+") "))
|
||||
}
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, os.Interrupt)
|
||||
defer signal.Stop(interrupt)
|
||||
|
||||
errCh := make(chan error)
|
||||
lineCh := make(chan string)
|
||||
go func() {
|
||||
var line string
|
||||
var err error
|
||||
inFile, valid := cmd.InOrStdin().(*os.File)
|
||||
if opts.Secret && valid && isatty.IsTerminal(inFile.Fd()) {
|
||||
line, err = speakeasy.Ask("")
|
||||
} else {
|
||||
reader := bufio.NewReader(cmd.InOrStdin())
|
||||
line, err = reader.ReadString('\n')
|
||||
// Multiline with single quotes!
|
||||
if err == nil && strings.HasPrefix(line, "'") {
|
||||
rest, err := reader.ReadString('\'')
|
||||
if err == nil {
|
||||
line += rest
|
||||
line = strings.Trim(line, "'")
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
if line == "" {
|
||||
line = opts.Default
|
||||
}
|
||||
lineCh <- line
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
return "", err
|
||||
case line := <-lineCh:
|
||||
if opts.IsConfirm && line != "yes" && line != "y" {
|
||||
return line, Canceled
|
||||
}
|
||||
if opts.Validate != nil {
|
||||
err := opts.Validate(line)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), defaultStyles.Error.Render(err.Error()))
|
||||
return Prompt(cmd, opts)
|
||||
}
|
||||
}
|
||||
return line, nil
|
||||
case <-cmd.Context().Done():
|
||||
return "", cmd.Context().Err()
|
||||
case <-interrupt:
|
||||
// Print a newline so that any further output starts properly on a new line.
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout())
|
||||
return "", Canceled
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package cliui_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/pty/ptytest"
|
||||
)
|
||||
|
||||
func TestPrompt(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ptty := ptytest.New(t)
|
||||
msgChan := make(chan string)
|
||||
go func() {
|
||||
resp, err := newPrompt(ptty, cliui.PromptOptions{
|
||||
Text: "Example",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
msgChan <- resp
|
||||
}()
|
||||
ptty.ExpectMatch("Example")
|
||||
ptty.WriteLine("hello")
|
||||
require.Equal(t, "hello", <-msgChan)
|
||||
})
|
||||
|
||||
t.Run("Confirm", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ptty := ptytest.New(t)
|
||||
doneChan := make(chan string)
|
||||
go func() {
|
||||
resp, err := newPrompt(ptty, cliui.PromptOptions{
|
||||
Text: "Example",
|
||||
IsConfirm: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
doneChan <- resp
|
||||
}()
|
||||
ptty.ExpectMatch("Example")
|
||||
ptty.WriteLine("yes")
|
||||
require.Equal(t, "yes", <-doneChan)
|
||||
})
|
||||
|
||||
t.Run("Multiline", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ptty := ptytest.New(t)
|
||||
doneChan := make(chan string)
|
||||
go func() {
|
||||
resp, err := newPrompt(ptty, cliui.PromptOptions{
|
||||
Text: "Example",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
doneChan <- resp
|
||||
}()
|
||||
ptty.ExpectMatch("Example")
|
||||
ptty.WriteLine("'this is a")
|
||||
ptty.WriteLine("test'")
|
||||
newline := "\n"
|
||||
if runtime.GOOS == "windows" {
|
||||
newline = "\r\n"
|
||||
}
|
||||
require.Equal(t, "this is a"+newline+"test", <-doneChan)
|
||||
})
|
||||
}
|
||||
|
||||
func newPrompt(ptty *ptytest.PTY, opts cliui.PromptOptions) (string, error) {
|
||||
value := ""
|
||||
cmd := &cobra.Command{
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
value, err = cliui.Prompt(cmd, opts)
|
||||
return err
|
||||
},
|
||||
}
|
||||
cmd.SetOutput(ptty.Output())
|
||||
cmd.SetIn(ptty.Input())
|
||||
return value, cmd.ExecuteContext(context.Background())
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package cliui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type SelectOptions struct {
|
||||
Options []string
|
||||
Size int
|
||||
HideSearch bool
|
||||
}
|
||||
|
||||
// Select displays a list of user options.
|
||||
func Select(cmd *cobra.Command, opts SelectOptions) (string, error) {
|
||||
selector := promptui.Select{
|
||||
Label: "",
|
||||
Items: opts.Options,
|
||||
Size: opts.Size,
|
||||
Searcher: func(input string, index int) bool {
|
||||
option := opts.Options[index]
|
||||
name := strings.Replace(strings.ToLower(option), " ", "", -1)
|
||||
input = strings.Replace(strings.ToLower(input), " ", "", -1)
|
||||
|
||||
return strings.Contains(name, input)
|
||||
},
|
||||
HideHelp: opts.HideSearch,
|
||||
Stdin: io.NopCloser(cmd.InOrStdin()),
|
||||
Stdout: &writeCloser{cmd.OutOrStdout()},
|
||||
Templates: &promptui.SelectTemplates{
|
||||
FuncMap: template.FuncMap{
|
||||
"faint": func(value interface{}) string {
|
||||
return Styles.Placeholder.Render(value.(string))
|
||||
},
|
||||
"subtle": func(value interface{}) string {
|
||||
return defaultStyles.Subtle.Render(value.(string))
|
||||
},
|
||||
"selected": func(value interface{}) string {
|
||||
return defaultStyles.Keyword.Render("> " + value.(string))
|
||||
// return defaultStyles.SelectedMenuItem.Render("> " + value.(string))
|
||||
},
|
||||
},
|
||||
Active: "{{ . | selected }}",
|
||||
Inactive: " {{ . }}",
|
||||
Label: "{{.}}",
|
||||
Selected: "{{ \"\" }}",
|
||||
Help: `{{ "Use" | faint }} {{ .SearchKey | faint }} {{ "to toggle search" | faint }}`,
|
||||
},
|
||||
HideSelected: true,
|
||||
}
|
||||
|
||||
_, result, err := selector.Run()
|
||||
if errors.Is(err, promptui.ErrAbort) || errors.Is(err, promptui.ErrInterrupt) {
|
||||
return result, Canceled
|
||||
}
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type writeCloser struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (*writeCloser) Close() error {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package cliui_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/pty/ptytest"
|
||||
)
|
||||
|
||||
func TestSelect(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Select", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ptty := ptytest.New(t)
|
||||
msgChan := make(chan string)
|
||||
go func() {
|
||||
resp, err := newSelect(ptty, cliui.SelectOptions{
|
||||
Options: []string{"First", "Second"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
msgChan <- resp
|
||||
}()
|
||||
ptty.ExpectMatch("Second")
|
||||
ptty.Write(promptui.KeyNext)
|
||||
ptty.WriteLine("")
|
||||
require.Equal(t, "Second", <-msgChan)
|
||||
})
|
||||
}
|
||||
|
||||
func newSelect(ptty *ptytest.PTY, opts cliui.SelectOptions) (string, error) {
|
||||
value := ""
|
||||
cmd := &cobra.Command{
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
value, err = cliui.Select(cmd, opts)
|
||||
return err
|
||||
},
|
||||
}
|
||||
cmd.SetOutput(ptty.Output())
|
||||
cmd.SetIn(ptty.Input())
|
||||
return value, cmd.ExecuteContext(context.Background())
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// const sshStartToken = "# ------------START-CODER-----------"
|
||||
// const sshStartMessage = `# This was generated by "coder config-ssh".
|
||||
// #
|
||||
// # To remove this blob, run:
|
||||
// #
|
||||
// # coder config-ssh --remove
|
||||
// #
|
||||
// # You should not hand-edit this section, unless you are deleting it.`
|
||||
// const sshEndToken = "# ------------END-CODER------------"
|
||||
|
||||
func configSSH() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "config-ssh",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
112
cli/daemon.go
112
cli/daemon.go
|
@ -1,112 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/database/databasefake"
|
||||
"github.com/coder/coder/provisioner/terraform"
|
||||
"github.com/coder/coder/provisionerd"
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
"github.com/coder/coder/provisionersdk/proto"
|
||||
)
|
||||
|
||||
func daemon() *cobra.Command {
|
||||
var (
|
||||
address string
|
||||
)
|
||||
root := &cobra.Command{
|
||||
Use: "daemon",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
logger := slog.Make(sloghuman.Sink(os.Stderr))
|
||||
accessURL := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: address,
|
||||
}
|
||||
handler, closeCoderd := coderd.New(&coderd.Options{
|
||||
AccessURL: accessURL,
|
||||
Logger: logger,
|
||||
Database: databasefake.New(),
|
||||
Pubsub: database.NewPubsubInMemory(),
|
||||
})
|
||||
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("listen %q: %w", address, err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
logger.Info(cmd.Context(), "daemon started", slog.F("url", accessURL.String()))
|
||||
|
||||
client := codersdk.New(accessURL)
|
||||
daemonClose, err := newProvisionerDaemon(cmd.Context(), client, logger)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create provisioner daemon: %w", err)
|
||||
}
|
||||
defer daemonClose.Close()
|
||||
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
errCh <- http.Serve(listener, handler)
|
||||
}()
|
||||
|
||||
closeCoderd()
|
||||
select {
|
||||
case <-cmd.Context().Done():
|
||||
return cmd.Context().Err()
|
||||
case err := <-errCh:
|
||||
return err
|
||||
}
|
||||
},
|
||||
}
|
||||
defaultAddress, ok := os.LookupEnv("ADDRESS")
|
||||
if !ok {
|
||||
defaultAddress = "127.0.0.1:3000"
|
||||
}
|
||||
root.Flags().StringVarP(&address, "address", "a", defaultAddress, "The address to serve the API and dashboard.")
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
func newProvisionerDaemon(ctx context.Context, client *codersdk.Client, logger slog.Logger) (io.Closer, error) {
|
||||
terraformClient, terraformServer := provisionersdk.TransportPipe()
|
||||
go func() {
|
||||
err := terraform.Serve(ctx, &terraform.ServeOptions{
|
||||
ServeOptions: &provisionersdk.ServeOptions{
|
||||
Listener: terraformServer,
|
||||
},
|
||||
Logger: logger,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
tempDir, err := ioutil.TempDir("", "provisionerd")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return provisionerd.New(client.ListenProvisionerDaemon, &provisionerd.Options{
|
||||
Logger: logger,
|
||||
PollInterval: 50 * time.Millisecond,
|
||||
UpdateInterval: 50 * time.Millisecond,
|
||||
Provisioners: provisionerd.Provisioners{
|
||||
string(database.ProvisionerTypeTerraform): proto.NewDRPCProvisionerClient(provisionersdk.Conn(terraformClient)),
|
||||
},
|
||||
WorkDirectory: tempDir,
|
||||
}), nil
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package cli_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/cli/clitest"
|
||||
)
|
||||
|
||||
func TestDaemon(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
go cancelFunc()
|
||||
root, _ := clitest.New(t, "daemon")
|
||||
err := root.ExecuteContext(ctx)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
}
|
64
cli/login.go
64
cli/login.go
|
@ -1,6 +1,7 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
|
@ -9,14 +10,12 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/pkg/browser"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
|
@ -67,38 +66,36 @@ func login() *cobra.Command {
|
|||
if !isTTY(cmd) {
|
||||
return xerrors.New("the initial user cannot be created in non-interactive mode. use the API")
|
||||
}
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Your Coder deployment hasn't been set up!\n", caret)
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), caret+"Your Coder deployment hasn't been set up!\n")
|
||||
|
||||
_, err := prompt(cmd, &promptui.Prompt{
|
||||
Label: "Would you like to create the first user?",
|
||||
_, err := cliui.Prompt(cmd, cliui.PromptOptions{
|
||||
Text: "Would you like to create the first user?",
|
||||
Default: "yes",
|
||||
IsConfirm: true,
|
||||
Default: "y",
|
||||
})
|
||||
if errors.Is(err, cliui.Canceled) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create user prompt: %w", err)
|
||||
return err
|
||||
}
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get current user: %w", err)
|
||||
}
|
||||
username, err := prompt(cmd, &promptui.Prompt{
|
||||
Label: "What username would you like?",
|
||||
username, err := cliui.Prompt(cmd, cliui.PromptOptions{
|
||||
Text: "What " + cliui.Styles.Field.Render("username") + " would you like?",
|
||||
Default: currentUser.Username,
|
||||
})
|
||||
if errors.Is(err, cliui.Canceled) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return xerrors.Errorf("pick username prompt: %w", err)
|
||||
}
|
||||
|
||||
organization, err := prompt(cmd, &promptui.Prompt{
|
||||
Label: "What is the name of your organization?",
|
||||
Default: "acme-corp",
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("pick organization prompt: %w", err)
|
||||
}
|
||||
|
||||
email, err := prompt(cmd, &promptui.Prompt{
|
||||
Label: "What's your email?",
|
||||
email, err := cliui.Prompt(cmd, cliui.PromptOptions{
|
||||
Text: "What's your " + cliui.Styles.Field.Render("email") + "?",
|
||||
Validate: func(s string) error {
|
||||
err := validator.New().Var(s, "email")
|
||||
if err != nil {
|
||||
|
@ -111,24 +108,25 @@ func login() *cobra.Command {
|
|||
return xerrors.Errorf("specify email prompt: %w", err)
|
||||
}
|
||||
|
||||
password, err := prompt(cmd, &promptui.Prompt{
|
||||
Label: "Enter a password:",
|
||||
Mask: '*',
|
||||
password, err := cliui.Prompt(cmd, cliui.PromptOptions{
|
||||
Text: "Enter a " + cliui.Styles.Field.Render("password") + ":",
|
||||
Secret: true,
|
||||
Validate: cliui.ValidateNotEmpty,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("specify password prompt: %w", err)
|
||||
}
|
||||
|
||||
_, err = client.CreateFirstUser(cmd.Context(), coderd.CreateFirstUserRequest{
|
||||
_, err = client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
|
||||
Email: email,
|
||||
Username: username,
|
||||
Organization: username,
|
||||
Password: password,
|
||||
Organization: organization,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create initial user: %w", err)
|
||||
}
|
||||
resp, err := client.LoginWithPassword(cmd.Context(), coderd.LoginWithPasswordRequest{
|
||||
resp, err := client.LoginWithPassword(cmd.Context(), codersdk.LoginWithPasswordRequest{
|
||||
Email: email,
|
||||
Password: password,
|
||||
})
|
||||
|
@ -147,7 +145,11 @@ func login() *cobra.Command {
|
|||
return xerrors.Errorf("write server url: %w", err)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're authenticated.\n", caret, color.HiCyanString(username))
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(),
|
||||
cliui.Styles.Paragraph.Render(fmt.Sprintf("Welcome to Coder, %s! You're authenticated.", cliui.Styles.Keyword.Render(username)))+"\n")
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(),
|
||||
cliui.Styles.Paragraph.Render("Get started by creating a project: "+cliui.Styles.Code.Render("coder projects create"))+"\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -159,9 +161,9 @@ func login() *cobra.Command {
|
|||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Your browser has been opened to visit:\n\n\t%s\n\n", authURL.String())
|
||||
}
|
||||
|
||||
sessionToken, err := prompt(cmd, &promptui.Prompt{
|
||||
Label: "Paste your token here:",
|
||||
Mask: '*',
|
||||
sessionToken, err := cliui.Prompt(cmd, cliui.PromptOptions{
|
||||
Text: "Paste your token here:",
|
||||
Secret: true,
|
||||
Validate: func(token string) error {
|
||||
client.SessionToken = token
|
||||
_, err := client.User(cmd.Context(), "me")
|
||||
|
@ -192,7 +194,7 @@ func login() *cobra.Command {
|
|||
return xerrors.Errorf("write server url: %w", err)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're authenticated.\n", caret, color.HiCyanString(resp.Username))
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), caret+"Welcome to Coder, %s! You're authenticated.\n", cliui.Styles.Keyword.Render(resp.Username))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package cli_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -26,19 +27,20 @@ func TestLogin(t *testing.T) {
|
|||
// The --force-tty flag is required on Windows, because the `isatty` library does not
|
||||
// accurately detect Windows ptys when they are not attached to a process:
|
||||
// https://github.com/mattn/go-isatty/issues/59
|
||||
root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty")
|
||||
doneChan := make(chan struct{})
|
||||
root, _ := clitest.New(t, "login", "--force-tty", client.URL.String())
|
||||
pty := ptytest.New(t)
|
||||
root.SetIn(pty.Input())
|
||||
root.SetOut(pty.Output())
|
||||
go func() {
|
||||
defer close(doneChan)
|
||||
err := root.Execute()
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
matches := []string{
|
||||
"first user?", "y",
|
||||
"first user?", "yes",
|
||||
"username", "testuser",
|
||||
"organization", "testorg",
|
||||
"email", "user@coder.com",
|
||||
"password", "password",
|
||||
}
|
||||
|
@ -49,6 +51,7 @@ func TestLogin(t *testing.T) {
|
|||
pty.WriteLine(value)
|
||||
}
|
||||
pty.ExpectMatch("Welcome to Coder")
|
||||
<-doneChan
|
||||
})
|
||||
|
||||
t.Run("ExistingUserValidTokenTTY", func(t *testing.T) {
|
||||
|
@ -56,11 +59,13 @@ func TestLogin(t *testing.T) {
|
|||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty", "--no-open")
|
||||
doneChan := make(chan struct{})
|
||||
root, _ := clitest.New(t, "login", "--force-tty", client.URL.String(), "--no-open")
|
||||
pty := ptytest.New(t)
|
||||
root.SetIn(pty.Input())
|
||||
root.SetOut(pty.Output())
|
||||
go func() {
|
||||
defer close(doneChan)
|
||||
err := root.Execute()
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
@ -68,6 +73,7 @@ func TestLogin(t *testing.T) {
|
|||
pty.ExpectMatch("Paste your token here:")
|
||||
pty.WriteLine(client.SessionToken)
|
||||
pty.ExpectMatch("Welcome to Coder")
|
||||
<-doneChan
|
||||
})
|
||||
|
||||
t.Run("ExistingUserInvalidTokenTTY", func(t *testing.T) {
|
||||
|
@ -75,12 +81,16 @@ func TestLogin(t *testing.T) {
|
|||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty", "--no-open")
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
doneChan := make(chan struct{})
|
||||
root, _ := clitest.New(t, "login", client.URL.String(), "--no-open")
|
||||
pty := ptytest.New(t)
|
||||
root.SetIn(pty.Input())
|
||||
root.SetOut(pty.Output())
|
||||
go func() {
|
||||
err := root.Execute()
|
||||
defer close(doneChan)
|
||||
err := root.ExecuteContext(ctx)
|
||||
// An error is expected in this case, since the login wasn't successful:
|
||||
require.Error(t, err)
|
||||
}()
|
||||
|
@ -88,5 +98,7 @@ func TestLogin(t *testing.T) {
|
|||
pty.ExpectMatch("Paste your token here:")
|
||||
pty.WriteLine("an-invalid-token")
|
||||
pty.ExpectMatch("That's not a valid token!")
|
||||
cancelFunc()
|
||||
<-doneChan
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
func parameterCreate() *cobra.Command {
|
||||
var (
|
||||
name string
|
||||
value string
|
||||
scheme string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "create <scope> [name]",
|
||||
Aliases: []string{"mk"},
|
||||
Args: cobra.ExactArgs(2),
|
||||
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
|
||||
}
|
||||
|
||||
scopeName := ""
|
||||
if len(args) >= 2 {
|
||||
scopeName = args[1]
|
||||
}
|
||||
scope, scopeID, err := parseScopeAndID(cmd.Context(), client, organization, args[0], scopeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scheme, err := parseParameterScheme(scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.CreateParameter(cmd.Context(), scope, scopeID, codersdk.CreateParameterRequest{
|
||||
Name: name,
|
||||
SourceValue: value,
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
DestinationScheme: scheme,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Printf("Created!\n")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&name, "name", "n", "", "Name for a parameter.")
|
||||
_ = cmd.MarkFlagRequired("name")
|
||||
cmd.Flags().StringVarP(&value, "value", "v", "", "Value for a parameter.")
|
||||
_ = cmd.MarkFlagRequired("value")
|
||||
cmd.Flags().StringVarP(&scheme, "scheme", "s", "var", `Scheme for the parameter ("var" or "env").`)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func parseParameterScheme(scheme string) (database.ParameterDestinationScheme, error) {
|
||||
switch scheme {
|
||||
case "env":
|
||||
return database.ParameterDestinationSchemeEnvironmentVariable, nil
|
||||
case "var":
|
||||
return database.ParameterDestinationSchemeProvisionerVariable, nil
|
||||
}
|
||||
return database.ParameterDestinationSchemeNone, xerrors.Errorf("scheme %q not recognized", scheme)
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package cli
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func parameterDelete() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "delete",
|
||||
Aliases: []string{"rm"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func parameterList() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list <scope> <scope-id>",
|
||||
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
|
||||
}
|
||||
|
||||
name := ""
|
||||
if len(args) >= 2 {
|
||||
name = args[1]
|
||||
}
|
||||
scope, scopeID, err := parseScopeAndID(cmd.Context(), client, organization, args[0], name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
params, err := client.Parameters(cmd.Context(), scope, scopeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writer := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 4, ' ', 0)
|
||||
_, _ = fmt.Fprintf(writer, "%s\t%s\t%s\n",
|
||||
color.HiBlackString("Parameter"),
|
||||
color.HiBlackString("Created"),
|
||||
color.HiBlackString("Scheme"))
|
||||
for _, param := range params {
|
||||
_, _ = fmt.Fprintf(writer, "%s\t%s\t%s\n",
|
||||
color.New(color.FgHiCyan).Sprint(param.Name),
|
||||
color.WhiteString(param.UpdatedAt.Format("January 2, 2006")),
|
||||
color.New(color.FgHiWhite).Sprint(param.DestinationScheme))
|
||||
}
|
||||
return writer.Flush()
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func parameters() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "parameters",
|
||||
Aliases: []string{"params"},
|
||||
}
|
||||
|
||||
cmd.AddCommand(parameterCreate(), parameterList(), parameterDelete())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func parseScopeAndID(ctx context.Context, client *codersdk.Client, organization codersdk.Organization, rawScope string, name string) (codersdk.ParameterScope, string, error) {
|
||||
scope, err := parseParameterScope(rawScope)
|
||||
if err != nil {
|
||||
return scope, "", err
|
||||
}
|
||||
var scopeID string
|
||||
switch scope {
|
||||
case codersdk.ParameterOrganization:
|
||||
if name == "" {
|
||||
scopeID = organization.ID
|
||||
} else {
|
||||
org, err := client.OrganizationByName(ctx, "", name)
|
||||
if err != nil {
|
||||
return scope, "", err
|
||||
}
|
||||
scopeID = org.ID
|
||||
}
|
||||
case codersdk.ParameterProject:
|
||||
project, err := client.ProjectByName(ctx, organization.ID, name)
|
||||
if err != nil {
|
||||
return scope, "", err
|
||||
}
|
||||
scopeID = project.ID.String()
|
||||
case codersdk.ParameterUser:
|
||||
user, err := client.User(ctx, name)
|
||||
if err != nil {
|
||||
return scope, "", err
|
||||
}
|
||||
scopeID = user.ID
|
||||
case codersdk.ParameterWorkspace:
|
||||
workspace, err := client.WorkspaceByName(ctx, "", name)
|
||||
if err != nil {
|
||||
return scope, "", err
|
||||
}
|
||||
scopeID = workspace.ID.String()
|
||||
}
|
||||
return scope, scopeID, nil
|
||||
}
|
||||
|
||||
func parseParameterScope(scope string) (codersdk.ParameterScope, error) {
|
||||
switch scope {
|
||||
case string(codersdk.ParameterOrganization):
|
||||
return codersdk.ParameterOrganization, nil
|
||||
case string(codersdk.ParameterProject):
|
||||
return codersdk.ParameterProject, nil
|
||||
case string(codersdk.ParameterUser):
|
||||
return codersdk.ParameterUser, nil
|
||||
case string(codersdk.ParameterWorkspace):
|
||||
return codersdk.ParameterWorkspace, nil
|
||||
}
|
||||
return codersdk.ParameterOrganization, xerrors.Errorf("no scope found by name %q", scope)
|
||||
}
|
|
@ -1,35 +1,34 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/fatih/color"
|
||||
"github.com/google/uuid"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/provisionerd"
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
)
|
||||
|
||||
func projectCreate() *cobra.Command {
|
||||
var (
|
||||
yes bool
|
||||
directory string
|
||||
provisioner string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Use: "create [name]",
|
||||
Short: "Create a project from the current directory",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := createClient(cmd)
|
||||
|
@ -40,70 +39,66 @@ func projectCreate() *cobra.Command {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = prompt(cmd, &promptui.Prompt{
|
||||
Default: "y",
|
||||
IsConfirm: true,
|
||||
Label: fmt.Sprintf("Set up %s in your organization?", color.New(color.FgHiCyan).Sprintf("%q", directory)),
|
||||
})
|
||||
|
||||
var projectName string
|
||||
if len(args) == 0 {
|
||||
projectName = filepath.Base(directory)
|
||||
} else {
|
||||
projectName = args[0]
|
||||
}
|
||||
_, err = client.ProjectByName(cmd.Context(), organization.ID, projectName)
|
||||
if err == nil {
|
||||
return xerrors.Errorf("A project already exists named %q!", projectName)
|
||||
}
|
||||
|
||||
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
|
||||
spin.Writer = cmd.OutOrStdout()
|
||||
spin.Suffix = cliui.Styles.Keyword.Render(" Uploading current directory...")
|
||||
spin.Start()
|
||||
defer spin.Stop()
|
||||
archive, err := provisionersdk.Tar(directory)
|
||||
if err != nil {
|
||||
if errors.Is(err, promptui.ErrAbort) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
name, err := prompt(cmd, &promptui.Prompt{
|
||||
Default: filepath.Base(directory),
|
||||
Label: "What's your project's name?",
|
||||
Validate: func(s string) error {
|
||||
project, _ := client.ProjectByName(cmd.Context(), organization.ID, s)
|
||||
if project.ID.String() != uuid.Nil.String() {
|
||||
return xerrors.New("A project already exists with that name!")
|
||||
resp, err := client.Upload(cmd.Context(), codersdk.ContentTypeTar, archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
spin.Stop()
|
||||
|
||||
spin = spinner.New(spinner.CharSets[5], 100*time.Millisecond)
|
||||
spin.Writer = cmd.OutOrStdout()
|
||||
spin.Suffix = cliui.Styles.Keyword.Render("Something")
|
||||
job, parameters, err := createValidProjectVersion(cmd, client, organization, database.ProvisionerType(provisioner), resp.Hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !yes {
|
||||
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
|
||||
Text: "Create project?",
|
||||
IsConfirm: true,
|
||||
Default: "yes",
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, promptui.ErrAbort) {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
job, err := validateProjectVersionSource(cmd, client, organization, database.ProvisionerType(provisioner), directory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
project, err := client.CreateProject(cmd.Context(), organization.ID, coderd.CreateProjectRequest{
|
||||
Name: name,
|
||||
VersionID: job.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = prompt(cmd, &promptui.Prompt{
|
||||
Label: "Create project?",
|
||||
IsConfirm: true,
|
||||
Default: "y",
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, promptui.ErrAbort) {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s The %s project has been created!\n", caret, color.HiCyanString(project.Name))
|
||||
_, err = prompt(cmd, &promptui.Prompt{
|
||||
Label: "Create a new workspace?",
|
||||
IsConfirm: true,
|
||||
Default: "y",
|
||||
_, err = client.CreateProject(cmd.Context(), organization.ID, codersdk.CreateProjectRequest{
|
||||
Name: projectName,
|
||||
VersionID: job.ID,
|
||||
ParameterValues: parameters,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, promptui.ErrAbort) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "The %s project has been created!\n", projectName)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -115,146 +110,93 @@ func projectCreate() *cobra.Command {
|
|||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cmd.Flags().BoolVarP(&yes, "yes", "y", false, "Bypass prompts")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, organization coderd.Organization, provisioner database.ProvisionerType, directory string, parameters ...coderd.CreateParameterRequest) (*coderd.ProjectVersion, error) {
|
||||
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
|
||||
spin.Writer = cmd.OutOrStdout()
|
||||
spin.Suffix = " Uploading current directory..."
|
||||
err := spin.Color("fgHiGreen")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spin.Start()
|
||||
defer spin.Stop()
|
||||
|
||||
tarData, err := tarDirectory(directory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := client.Upload(cmd.Context(), codersdk.ContentTypeTar, tarData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func createValidProjectVersion(cmd *cobra.Command, client *codersdk.Client, organization codersdk.Organization, provisioner database.ProvisionerType, hash string, parameters ...codersdk.CreateParameterRequest) (*codersdk.ProjectVersion, []codersdk.CreateParameterRequest, error) {
|
||||
before := time.Now()
|
||||
version, err := client.CreateProjectVersion(cmd.Context(), organization.ID, coderd.CreateProjectVersionRequest{
|
||||
version, err := client.CreateProjectVersion(cmd.Context(), organization.ID, codersdk.CreateProjectVersionRequest{
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: resp.Hash,
|
||||
StorageSource: hash,
|
||||
Provisioner: provisioner,
|
||||
ParameterValues: parameters,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spin.Suffix = " Waiting for the import to complete..."
|
||||
logs, err := client.ProjectVersionLogsAfter(cmd.Context(), version.ID, before)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logBuffer := make([]coderd.ProvisionerJobLog, 0, 64)
|
||||
for {
|
||||
log, ok := <-logs
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
logBuffer = append(logBuffer, log)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
_, err = cliui.Job(cmd, cliui.JobOptions{
|
||||
Title: "Building project...",
|
||||
Fetch: func() (codersdk.ProvisionerJob, error) {
|
||||
version, err := client.ProjectVersion(cmd.Context(), version.ID)
|
||||
return version.Job, err
|
||||
},
|
||||
Cancel: func() error {
|
||||
return client.CancelProjectVersion(cmd.Context(), version.ID)
|
||||
},
|
||||
Logs: func() (<-chan codersdk.ProvisionerJobLog, error) {
|
||||
return client.ProjectVersionLogsAfter(cmd.Context(), version.ID, before)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
version, err = client.ProjectVersion(cmd.Context(), version.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
parameterSchemas, err := client.ProjectVersionSchema(cmd.Context(), version.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
parameterValues, err := client.ProjectVersionParameters(cmd.Context(), version.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
spin.Stop()
|
||||
|
||||
if provisionerd.IsMissingParameterError(version.Job.Error) {
|
||||
valuesBySchemaID := map[string]coderd.ProjectVersionParameter{}
|
||||
valuesBySchemaID := map[string]codersdk.ProjectVersionParameter{}
|
||||
for _, parameterValue := range parameterValues {
|
||||
valuesBySchemaID[parameterValue.SchemaID.String()] = parameterValue
|
||||
}
|
||||
sort.Slice(parameterSchemas, func(i, j int) bool {
|
||||
return parameterSchemas[i].Name < parameterSchemas[j].Name
|
||||
})
|
||||
missingSchemas := make([]codersdk.ProjectVersionParameterSchema, 0)
|
||||
for _, parameterSchema := range parameterSchemas {
|
||||
_, ok := valuesBySchemaID[parameterSchema.ID.String()]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
value, err := prompt(cmd, &promptui.Prompt{
|
||||
Label: fmt.Sprintf("Enter value for %s:", color.HiCyanString(parameterSchema.Name)),
|
||||
})
|
||||
missingSchemas = append(missingSchemas, parameterSchema)
|
||||
}
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("This project has required variables! They are scoped to the project, and not viewable after being set.")+"\r\n")
|
||||
for _, parameterSchema := range missingSchemas {
|
||||
value, err := cliui.ParameterSchema(cmd, parameterSchema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
parameters = append(parameters, coderd.CreateParameterRequest{
|
||||
parameters = append(parameters, codersdk.CreateParameterRequest{
|
||||
Name: parameterSchema.Name,
|
||||
SourceValue: value,
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
DestinationScheme: parameterSchema.DefaultDestinationScheme,
|
||||
})
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout())
|
||||
}
|
||||
return validateProjectVersionSource(cmd, client, organization, provisioner, directory, parameters...)
|
||||
return createValidProjectVersion(cmd, client, organization, provisioner, hash, parameters...)
|
||||
}
|
||||
|
||||
if version.Job.Status != coderd.ProvisionerJobSucceeded {
|
||||
for _, log := range logBuffer {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s %s\n", color.HiGreenString("[tf]"), log.Output)
|
||||
}
|
||||
return nil, xerrors.New(version.Job.Error)
|
||||
if version.Job.Status != codersdk.ProvisionerJobSucceeded {
|
||||
return nil, nil, xerrors.New(version.Job.Error)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Successfully imported project source!\n", color.HiGreenString("✓"))
|
||||
|
||||
resources, err := client.ProjectVersionResources(cmd.Context(), version.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
return &version, displayProjectImportInfo(cmd, parameterSchemas, parameterValues, resources)
|
||||
}
|
||||
|
||||
func tarDirectory(directory string) ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
tarWriter := tar.NewWriter(&buffer)
|
||||
err := filepath.Walk(directory, func(file string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header, err := tar.FileInfoHeader(fileInfo, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(directory, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = rel
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
return nil
|
||||
}
|
||||
data, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(tarWriter, data); err != nil {
|
||||
return err
|
||||
}
|
||||
return data.Close()
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = tarWriter.Flush()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
return &version, parameters, displayProjectVersionInfo(cmd, resources)
|
||||
}
|
||||
|
|
|
@ -9,13 +9,12 @@ import (
|
|||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/provisioner/echo"
|
||||
"github.com/coder/coder/provisionersdk/proto"
|
||||
"github.com/coder/coder/pty/ptytest"
|
||||
)
|
||||
|
||||
func TestProjectCreate(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NoParameters", func(t *testing.T) {
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
|
@ -23,24 +22,20 @@ func TestProjectCreate(t *testing.T) {
|
|||
Parse: echo.ParseComplete,
|
||||
Provision: echo.ProvisionComplete,
|
||||
})
|
||||
cmd, root := clitest.New(t, "projects", "create", "--directory", source, "--provisioner", string(database.ProvisionerTypeEcho))
|
||||
cmd, root := clitest.New(t, "projects", "create", "my-project", "--directory", source, "--provisioner", string(database.ProvisionerTypeEcho))
|
||||
clitest.SetupConfig(t, client, root)
|
||||
_ = coderdtest.NewProvisionerDaemon(t, client)
|
||||
doneChan := make(chan struct{})
|
||||
pty := ptytest.New(t)
|
||||
cmd.SetIn(pty.Input())
|
||||
cmd.SetOut(pty.Output())
|
||||
closeChan := make(chan struct{})
|
||||
go func() {
|
||||
defer close(doneChan)
|
||||
err := cmd.Execute()
|
||||
require.NoError(t, err)
|
||||
close(closeChan)
|
||||
}()
|
||||
|
||||
matches := []string{
|
||||
"organization?", "y",
|
||||
"name?", "test-project",
|
||||
"project?", "y",
|
||||
"created!", "n",
|
||||
"Create project?", "yes",
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
match := matches[i]
|
||||
|
@ -48,54 +43,6 @@ func TestProjectCreate(t *testing.T) {
|
|||
pty.ExpectMatch(match)
|
||||
pty.WriteLine(value)
|
||||
}
|
||||
<-closeChan
|
||||
})
|
||||
|
||||
t.Run("Parameter", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
source := clitest.CreateProjectVersionSource(t, &echo.Responses{
|
||||
Parse: []*proto.Parse_Response{{
|
||||
Type: &proto.Parse_Response_Complete{
|
||||
Complete: &proto.Parse_Complete{
|
||||
ParameterSchemas: []*proto.ParameterSchema{{
|
||||
Name: "somevar",
|
||||
DefaultDestination: &proto.ParameterDestination{
|
||||
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
Provision: echo.ProvisionComplete,
|
||||
})
|
||||
cmd, root := clitest.New(t, "projects", "create", "--directory", source, "--provisioner", string(database.ProvisionerTypeEcho))
|
||||
clitest.SetupConfig(t, client, root)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
pty := ptytest.New(t)
|
||||
cmd.SetIn(pty.Input())
|
||||
cmd.SetOut(pty.Output())
|
||||
closeChan := make(chan struct{})
|
||||
go func() {
|
||||
err := cmd.Execute()
|
||||
require.NoError(t, err)
|
||||
close(closeChan)
|
||||
}()
|
||||
|
||||
matches := []string{
|
||||
"organization?", "y",
|
||||
"name?", "test-project",
|
||||
"somevar", "value",
|
||||
"project?", "y",
|
||||
"created!", "n",
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
match := matches[i]
|
||||
value := matches[i+1]
|
||||
pty.ExpectMatch(match)
|
||||
pty.WriteLine(value)
|
||||
}
|
||||
<-closeChan
|
||||
<-doneChan
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package cli
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func projectEdit() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "edit",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/examples"
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
)
|
||||
|
||||
func projectInit() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "init [directory]",
|
||||
Short: "Get started with a templated project.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
exampleList, err := examples.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exampleNames := []string{}
|
||||
exampleByName := map[string]examples.Example{}
|
||||
for _, example := range exampleList {
|
||||
exampleNames = append(exampleNames, example.Name)
|
||||
exampleByName[example.Name] = example
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Wrap.Render("Projects contain Infrastructure as Code that works with Coder to provision development workspaces. Get started by selecting an example:\n"))
|
||||
option, err := cliui.Select(cmd, cliui.SelectOptions{
|
||||
Options: exampleNames,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
selectedTemplate := exampleByName[option]
|
||||
archive, err := examples.Archive(selectedTemplate.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workingDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var directory string
|
||||
if len(args) > 0 {
|
||||
directory = args[0]
|
||||
} else {
|
||||
directory = filepath.Join(workingDir, selectedTemplate.ID)
|
||||
}
|
||||
relPath, err := filepath.Rel(workingDir, directory)
|
||||
if err != nil {
|
||||
relPath = directory
|
||||
} else {
|
||||
relPath = "./" + relPath
|
||||
}
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%sExtracting %s to %s...\n", cliui.Styles.Prompt, cliui.Styles.Field.Render(selectedTemplate.ID), cliui.Styles.Keyword.Render(relPath))
|
||||
err = os.MkdirAll(directory, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = provisionersdk.Untar(directory, archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"Inside that directory, get started by running:")
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render(cliui.Styles.Code.Render("coder projects create"))+"\n")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package cli_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/cli/clitest"
|
||||
"github.com/coder/coder/pty/ptytest"
|
||||
)
|
||||
|
||||
func TestProjectInit(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Extract", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tempDir := t.TempDir()
|
||||
cmd, _ := clitest.New(t, "projects", "init", tempDir)
|
||||
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("Develop in Linux")
|
||||
pty.WriteLine("")
|
||||
<-doneChan
|
||||
files, err := os.ReadDir(tempDir)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(files), 0)
|
||||
})
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
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 TestProjectList(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("None", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
cmd, root := clitest.New(t, "projects", "list")
|
||||
clitest.SetupConfig(t, client, root)
|
||||
pty := ptytest.New(t)
|
||||
cmd.SetIn(pty.Input())
|
||||
cmd.SetOut(pty.Output())
|
||||
closeChan := make(chan struct{})
|
||||
go func() {
|
||||
err := cmd.Execute()
|
||||
require.NoError(t, err)
|
||||
close(closeChan)
|
||||
}()
|
||||
pty.ExpectMatch("No projects found")
|
||||
<-closeChan
|
||||
})
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
daemon := coderdtest.NewProvisionerDaemon(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
_ = daemon.Close()
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
cmd, root := clitest.New(t, "projects", "list")
|
||||
clitest.SetupConfig(t, client, root)
|
||||
pty := ptytest.New(t)
|
||||
cmd.SetIn(pty.Input())
|
||||
cmd.SetOut(pty.Output())
|
||||
closeChan := make(chan struct{})
|
||||
go func() {
|
||||
err := cmd.Execute()
|
||||
require.NoError(t, err)
|
||||
close(closeChan)
|
||||
}()
|
||||
pty.ExpectMatch(project.Name)
|
||||
<-closeChan
|
||||
})
|
||||
}
|
|
@ -2,14 +2,13 @@ package cli
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sort"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/xlab/treeprint"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
|
@ -32,53 +31,53 @@ func projects() *cobra.Command {
|
|||
}
|
||||
cmd.AddCommand(
|
||||
projectCreate(),
|
||||
projectEdit(),
|
||||
projectInit(),
|
||||
projectList(),
|
||||
projectPlan(),
|
||||
projectUpdate(),
|
||||
projectVersions(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func displayProjectImportInfo(cmd *cobra.Command, parameterSchemas []coderd.ProjectVersionParameterSchema, parameterValues []coderd.ProjectVersionParameter, resources []coderd.WorkspaceResource) error {
|
||||
schemaByID := map[string]coderd.ProjectVersionParameterSchema{}
|
||||
for _, schema := range parameterSchemas {
|
||||
schemaByID[schema.ID.String()] = schema
|
||||
}
|
||||
func displayProjectVersionInfo(cmd *cobra.Command, resources []codersdk.WorkspaceResource) error {
|
||||
sort.Slice(resources, func(i, j int) bool {
|
||||
return fmt.Sprintf("%s.%s", resources[i].Type, resources[i].Name) < fmt.Sprintf("%s.%s", resources[j].Type, resources[j].Name)
|
||||
})
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n %s\n\n", color.HiBlackString("Parameters"))
|
||||
for _, value := range parameterValues {
|
||||
schema, ok := schemaByID[value.SchemaID.String()]
|
||||
if !ok {
|
||||
return xerrors.Errorf("schema not found: %s", value.Name)
|
||||
}
|
||||
displayValue := value.SourceValue
|
||||
if !schema.RedisplayValue {
|
||||
displayValue = "<redacted>"
|
||||
}
|
||||
output := fmt.Sprintf("%s %s %s", color.HiCyanString(value.Name), color.HiBlackString("="), displayValue)
|
||||
if value.DefaultSourceValue {
|
||||
output += " (default value)"
|
||||
} else if value.Scope != database.ParameterScopeImportJob {
|
||||
output += fmt.Sprintf(" (inherited from %s)", value.Scope)
|
||||
}
|
||||
|
||||
root := treeprint.NewWithRoot(output)
|
||||
if schema.Description != "" {
|
||||
root.AddBranch(fmt.Sprintf("%s\n%s", color.HiBlackString("Description"), schema.Description))
|
||||
}
|
||||
if schema.AllowOverrideSource {
|
||||
root.AddBranch(fmt.Sprintf("%s Users can customize this value!", color.HiYellowString("+")))
|
||||
}
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+strings.Join(strings.Split(root.String(), "\n"), "\n "))
|
||||
}
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " %s\n\n", color.HiBlackString("Resources"))
|
||||
addressOnStop := map[string]codersdk.WorkspaceResource{}
|
||||
for _, resource := range resources {
|
||||
transition := color.HiGreenString("start")
|
||||
if resource.Transition == database.WorkspaceTransitionStop {
|
||||
transition = color.HiRedString("stop")
|
||||
if resource.Transition != database.WorkspaceTransitionStop {
|
||||
continue
|
||||
}
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " %s %s on %s\n\n", color.HiCyanString(resource.Type), color.HiCyanString(resource.Name), transition)
|
||||
addressOnStop[resource.Address] = resource
|
||||
}
|
||||
|
||||
displayed := map[string]struct{}{}
|
||||
for _, resource := range resources {
|
||||
if resource.Type == "random_string" {
|
||||
// Hide resources that aren't substantial to a user!
|
||||
continue
|
||||
}
|
||||
_, alreadyShown := displayed[resource.Address]
|
||||
if alreadyShown {
|
||||
continue
|
||||
}
|
||||
displayed[resource.Address] = struct{}{}
|
||||
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Bold.Render("resource."+resource.Type+"."+resource.Name))
|
||||
_, existsOnStop := addressOnStop[resource.Address]
|
||||
if existsOnStop {
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Warn.Render("~ persistent"))
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Keyword.Render("+ start")+cliui.Styles.Placeholder.Render(" (deletes on stop)"))
|
||||
}
|
||||
if resource.Agent != nil {
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Fuschia.Render("▲ allows ssh"))
|
||||
}
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,13 +1,94 @@
|
|||
package cli
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
)
|
||||
|
||||
func projectUpdate() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "update <name>",
|
||||
Use: "update <project> [directory]",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "Update a project from the current directory",
|
||||
Short: "Update the source-code of a project from a directory.",
|
||||
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
|
||||
}
|
||||
project, err := client.ProjectByName(cmd.Context(), organization.ID, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
directory, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(args) >= 2 {
|
||||
directory, err = filepath.Abs(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
content, err := provisionersdk.Tar(directory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := client.Upload(cmd.Context(), codersdk.ContentTypeTar, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
before := time.Now()
|
||||
projectVersion, err := client.CreateProjectVersion(cmd.Context(), organization.ID, codersdk.CreateProjectVersionRequest{
|
||||
ProjectID: project.ID,
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: resp.Hash,
|
||||
Provisioner: database.ProvisionerTypeTerraform,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logs, err := client.ProjectVersionLogsAfter(cmd.Context(), projectVersion.ID, before)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
log, ok := <-logs
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
_, _ = fmt.Printf("terraform (%s): %s\n", log.Level, log.Output)
|
||||
}
|
||||
projectVersion, err = client.ProjectVersion(cmd.Context(), projectVersion.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if projectVersion.Job.Status != codersdk.ProvisionerJobSucceeded {
|
||||
return xerrors.New("job failed")
|
||||
}
|
||||
|
||||
err = client.UpdateActiveProjectVersion(cmd.Context(), project.ID, codersdk.UpdateActiveProjectVersion{
|
||||
ID: projectVersion.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Printf("Updated version!\n")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package cli
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func projectVersions() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "versions",
|
||||
Aliases: []string{"version"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// coder project versions
|
133
cli/root.go
133
cli/root.go
|
@ -1,25 +1,22 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kirsle/configdir"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/cli/config"
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
var (
|
||||
caret = color.HiBlackString(">")
|
||||
caret = cliui.Styles.Prompt.String()
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -30,47 +27,48 @@ const (
|
|||
|
||||
func Root() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "coder",
|
||||
Use: "coder",
|
||||
SilenceUsage: true,
|
||||
Long: ` ▄█▀ ▀█▄
|
||||
▄▄ ▀▀▀ █▌ ██▀▀█▄ ▐█
|
||||
▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀
|
||||
█▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █
|
||||
██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀
|
||||
` + color.New(color.Underline).Sprint("Self-hosted developer workspaces on your infra") + `
|
||||
` + lipgloss.NewStyle().Underline(true).Render("Self-hosted developer workspaces on your infra") + `
|
||||
|
||||
`,
|
||||
Example: `
|
||||
- Create a project for developers to create workspaces
|
||||
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.`) + `
|
||||
|
||||
` + color.New(color.FgHiMagenta).Sprint("$ coder projects create <directory>") + `
|
||||
` + cliui.Styles.Code.Render("$ coder start --dev") + `
|
||||
` + cliui.Styles.Paragraph.Render("Get started by creating a project from an example.") + `
|
||||
|
||||
- Create a workspace for a specific project
|
||||
|
||||
` + color.New(color.FgHiMagenta).Sprint("$ coder workspaces create <project>") + `
|
||||
|
||||
- Maintain consistency by updating a workspace
|
||||
|
||||
` + color.New(color.FgHiMagenta).Sprint("$ coder workspaces update <workspace>"),
|
||||
` + cliui.Styles.Code.Render("$ coder projects init"),
|
||||
}
|
||||
// Customizes the color of headings to make subcommands
|
||||
// more visually appealing.
|
||||
header := color.New(color.FgHiBlack)
|
||||
header := cliui.Styles.Placeholder
|
||||
cmd.SetUsageTemplate(strings.NewReplacer(
|
||||
`Usage:`, header.Sprint("Usage:"),
|
||||
`Examples:`, header.Sprint("Examples:"),
|
||||
`Available Commands:`, header.Sprint("Commands:"),
|
||||
`Global Flags:`, header.Sprint("Global Flags:"),
|
||||
`Flags:`, header.Sprint("Flags:"),
|
||||
`Additional help topics:`, header.Sprint("Additional help:"),
|
||||
`Usage:`, header.Render("Usage:"),
|
||||
`Examples:`, header.Render("Examples:"),
|
||||
`Available Commands:`, header.Render("Commands:"),
|
||||
`Global Flags:`, header.Render("Global Flags:"),
|
||||
`Flags:`, header.Render("Flags:"),
|
||||
`Additional help topics:`, header.Render("Additional help:"),
|
||||
).Replace(cmd.UsageTemplate()))
|
||||
|
||||
cmd.AddCommand(daemon())
|
||||
cmd.AddCommand(login())
|
||||
cmd.AddCommand(projects())
|
||||
cmd.AddCommand(workspaces())
|
||||
cmd.AddCommand(users())
|
||||
cmd.AddCommand(
|
||||
configSSH(),
|
||||
start(),
|
||||
login(),
|
||||
parameters(),
|
||||
projects(),
|
||||
users(),
|
||||
workspaces(),
|
||||
workspaceSSH(),
|
||||
workspaceTunnel(),
|
||||
)
|
||||
|
||||
cmd.PersistentFlags().String(varGlobalConfig, configdir.LocalConfig("coder"), "Path to the global `coder` config directory")
|
||||
cmd.PersistentFlags().String(varGlobalConfig, configdir.LocalConfig("coderv2"), "Path to the global `coder` config directory")
|
||||
cmd.PersistentFlags().Bool(varForceTty, false, "Force the `coder` command to run as if connected to a TTY")
|
||||
err := cmd.PersistentFlags().MarkHidden(varForceTty)
|
||||
if err != nil {
|
||||
|
@ -108,10 +106,10 @@ func createClient(cmd *cobra.Command) (*codersdk.Client, error) {
|
|||
}
|
||||
|
||||
// currentOrganization returns the currently active organization for the authenticated user.
|
||||
func currentOrganization(cmd *cobra.Command, client *codersdk.Client) (coderd.Organization, error) {
|
||||
func currentOrganization(cmd *cobra.Command, client *codersdk.Client) (codersdk.Organization, error) {
|
||||
orgs, err := client.OrganizationsByUser(cmd.Context(), "me")
|
||||
if err != nil {
|
||||
return coderd.Organization{}, nil
|
||||
return codersdk.Organization{}, nil
|
||||
}
|
||||
// For now, we won't use the config to set this.
|
||||
// Eventually, we will support changing using "coder switch <org>"
|
||||
|
@ -138,76 +136,9 @@ func isTTY(cmd *cobra.Command) bool {
|
|||
if forceTty && err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
reader := cmd.InOrStdin()
|
||||
file, ok := reader.(*os.File)
|
||||
file, ok := cmd.InOrStdin().(*os.File)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return isatty.IsTerminal(file.Fd())
|
||||
}
|
||||
|
||||
func prompt(cmd *cobra.Command, prompt *promptui.Prompt) (string, error) {
|
||||
prompt.Stdin = io.NopCloser(cmd.InOrStdin())
|
||||
prompt.Stdout = readWriteCloser{
|
||||
Writer: cmd.OutOrStdout(),
|
||||
}
|
||||
|
||||
// The prompt library displays defaults in a jarring way for the user
|
||||
// by attempting to autocomplete it. This sets no default enabling us
|
||||
// to customize the display.
|
||||
defaultValue := prompt.Default
|
||||
if !prompt.IsConfirm {
|
||||
prompt.Default = ""
|
||||
}
|
||||
|
||||
// Rewrite the confirm template to remove bold, and fit to the Coder style.
|
||||
confirmEnd := fmt.Sprintf("[y/%s] ", color.New(color.Bold).Sprint("N"))
|
||||
if prompt.Default == "y" {
|
||||
confirmEnd = fmt.Sprintf("[%s/n] ", color.New(color.Bold).Sprint("Y"))
|
||||
}
|
||||
confirm := color.HiBlackString("?") + ` {{ . }} ` + confirmEnd
|
||||
|
||||
// Customize to remove bold.
|
||||
valid := color.HiBlackString("?") + " {{ . }} "
|
||||
if defaultValue != "" {
|
||||
valid += fmt.Sprintf("(%s) ", defaultValue)
|
||||
}
|
||||
|
||||
success := valid
|
||||
invalid := valid
|
||||
if prompt.IsConfirm {
|
||||
success = confirm
|
||||
invalid = confirm
|
||||
}
|
||||
|
||||
prompt.Templates = &promptui.PromptTemplates{
|
||||
Confirm: confirm,
|
||||
Success: success,
|
||||
Invalid: invalid,
|
||||
Valid: valid,
|
||||
}
|
||||
oldValidate := prompt.Validate
|
||||
if oldValidate != nil {
|
||||
// Override the validate function to pass our default!
|
||||
prompt.Validate = func(s string) error {
|
||||
if s == "" {
|
||||
s = defaultValue
|
||||
}
|
||||
return oldValidate(s)
|
||||
}
|
||||
}
|
||||
value, err := prompt.Run()
|
||||
if value == "" && !prompt.IsConfirm {
|
||||
value = defaultValue
|
||||
}
|
||||
|
||||
return value, err
|
||||
}
|
||||
|
||||
// readWriteCloser fakes reads, writes, and closing!
|
||||
type readWriteCloser struct {
|
||||
io.Reader
|
||||
io.Writer
|
||||
io.Closer
|
||||
}
|
||||
|
|
87
cli/ssh.go
87
cli/ssh.go
|
@ -1 +1,88 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/coder/coder/agent"
|
||||
"github.com/coder/coder/peer"
|
||||
"github.com/coder/coder/peerbroker"
|
||||
)
|
||||
|
||||
func workspaceSSH() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "ssh <workspace> [resource]",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := createClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workspace, err := client.WorkspaceByName(cmd.Context(), "", args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resources, err := client.WorkspaceResourcesByBuild(cmd.Context(), workspace.LatestBuild.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, resource := range resources {
|
||||
_, _ = fmt.Printf("Got resource: %+v\n", resource)
|
||||
if resource.Agent == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
dialed, err := client.DialWorkspaceAgent(cmd.Context(), resource.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stream, err := dialed.NegotiateConnection(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn, err := peerbroker.Dial(stream, []webrtc.ICEServer{{
|
||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||
}}, &peer.ConnOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := agent.DialSSHClient(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = term.MakeRaw(int(os.Stdin.Fd()))
|
||||
err = session.RequestPty("xterm-256color", 128, 128, ssh.TerminalModes{
|
||||
ssh.OCRNL: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session.Stdin = os.Stdin
|
||||
session.Stdout = os.Stdout
|
||||
session.Stderr = os.Stderr
|
||||
err = session.Shell()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = session.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
"google.golang.org/api/idtoken"
|
||||
"google.golang.org/api/option"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/tunnel"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/database/databasefake"
|
||||
"github.com/coder/coder/provisioner/terraform"
|
||||
"github.com/coder/coder/provisionerd"
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
"github.com/coder/coder/provisionersdk/proto"
|
||||
)
|
||||
|
||||
func start() *cobra.Command {
|
||||
var (
|
||||
address string
|
||||
dev bool
|
||||
useTunnel bool
|
||||
)
|
||||
root := &cobra.Command{
|
||||
Use: "start",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
logger := slog.Make(sloghuman.Sink(os.Stderr))
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("listen %q: %w", address, err)
|
||||
}
|
||||
defer listener.Close()
|
||||
tcpAddr, valid := listener.Addr().(*net.TCPAddr)
|
||||
if !valid {
|
||||
return xerrors.New("must be listening on tcp")
|
||||
}
|
||||
if tcpAddr.IP.IsUnspecified() {
|
||||
tcpAddr.IP = net.IPv4(127, 0, 0, 1)
|
||||
}
|
||||
|
||||
localURL := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: tcpAddr.String(),
|
||||
}
|
||||
accessURL := localURL
|
||||
var tunnelErr <-chan error
|
||||
if dev {
|
||||
if useTunnel {
|
||||
var accessURLRaw string
|
||||
accessURLRaw, tunnelErr, err = tunnel.New(cmd.Context(), localURL.String())
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create tunnel: %w", err)
|
||||
}
|
||||
accessURL, err = url.Parse(accessURLRaw)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("parse: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), ` ▄█▀ ▀█▄
|
||||
▄▄ ▀▀▀ █▌ ██▀▀█▄ ▐█
|
||||
▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀
|
||||
█▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █
|
||||
██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀
|
||||
|
||||
`+cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.Prompt.String()+`Started in `+
|
||||
cliui.Styles.Field.Render("dev")+` mode. All data is in-memory! Learn how to setup and manage a production Coder deployment here: `+cliui.Styles.Prompt.Render("https://coder.com/docs/TODO")))+
|
||||
`
|
||||
`+
|
||||
cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.FocusedPrompt.String()+`Run `+cliui.Styles.Code.Render("coder projects init")+" in a new terminal to get started.\n"))+`
|
||||
`)
|
||||
}
|
||||
|
||||
validator, err := idtoken.NewValidator(cmd.Context(), option.WithoutAuthentication())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
handler, closeCoderd := coderd.New(&coderd.Options{
|
||||
AccessURL: accessURL,
|
||||
Logger: logger,
|
||||
Database: databasefake.New(),
|
||||
Pubsub: database.NewPubsubInMemory(),
|
||||
GoogleTokenValidator: validator,
|
||||
})
|
||||
client := codersdk.New(localURL)
|
||||
|
||||
daemonClose, err := newProvisionerDaemon(cmd.Context(), client, logger)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create provisioner daemon: %w", err)
|
||||
}
|
||||
defer daemonClose.Close()
|
||||
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
errCh <- http.Serve(listener, handler)
|
||||
}()
|
||||
|
||||
if dev {
|
||||
config := createConfig(cmd)
|
||||
_, err = client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
|
||||
Email: "dev@coder.com",
|
||||
Username: "developer",
|
||||
Password: "password",
|
||||
Organization: "coder",
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create first user: %w\n", err)
|
||||
}
|
||||
token, err := client.LoginWithPassword(cmd.Context(), codersdk.LoginWithPasswordRequest{
|
||||
Email: "dev@coder.com",
|
||||
Password: "password",
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("login with first user: %w", err)
|
||||
}
|
||||
err = config.URL().Write(localURL.String())
|
||||
if err != nil {
|
||||
return xerrors.Errorf("write local url: %w", err)
|
||||
}
|
||||
err = config.Session().Write(token.SessionToken)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("write session token: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
closeCoderd()
|
||||
select {
|
||||
case <-cmd.Context().Done():
|
||||
return cmd.Context().Err()
|
||||
case err := <-tunnelErr:
|
||||
return err
|
||||
case err := <-errCh:
|
||||
return err
|
||||
}
|
||||
},
|
||||
}
|
||||
defaultAddress, ok := os.LookupEnv("ADDRESS")
|
||||
if !ok {
|
||||
defaultAddress = "127.0.0.1:3000"
|
||||
}
|
||||
root.Flags().StringVarP(&address, "address", "a", defaultAddress, "The address to serve the API and dashboard.")
|
||||
root.Flags().BoolVarP(&dev, "dev", "", false, "Serve Coder in dev mode for tinkering.")
|
||||
root.Flags().BoolVarP(&useTunnel, "tunnel", "", true, `Serve "dev" mode through a Cloudflare Tunnel for easy setup.`)
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
func newProvisionerDaemon(ctx context.Context, client *codersdk.Client, logger slog.Logger) (io.Closer, error) {
|
||||
terraformClient, terraformServer := provisionersdk.TransportPipe()
|
||||
go func() {
|
||||
err := terraform.Serve(ctx, &terraform.ServeOptions{
|
||||
ServeOptions: &provisionersdk.ServeOptions{
|
||||
Listener: terraformServer,
|
||||
},
|
||||
Logger: logger,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
tempDir, err := ioutil.TempDir("", "provisionerd")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return provisionerd.New(client.ListenProvisionerDaemon, &provisionerd.Options{
|
||||
Logger: logger,
|
||||
PollInterval: 50 * time.Millisecond,
|
||||
UpdateInterval: 50 * time.Millisecond,
|
||||
Provisioners: provisionerd.Provisioners{
|
||||
string(database.ProvisionerTypeTerraform): proto.NewDRPCProvisionerClient(provisionersdk.Conn(terraformClient)),
|
||||
},
|
||||
WorkDirectory: tempDir,
|
||||
}), nil
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package cli_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/cli/clitest"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func TestStart(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Production", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
go cancelFunc()
|
||||
root, _ := clitest.New(t, "start", "--address", ":0")
|
||||
err := root.ExecuteContext(ctx)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
})
|
||||
t.Run("Development", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
root, cfg := clitest.New(t, "start", "--dev", "--tunnel=false", "--address", ":0")
|
||||
go func() {
|
||||
err := root.ExecuteContext(ctx)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
}()
|
||||
var accessURL string
|
||||
require.Eventually(t, func() bool {
|
||||
var err error
|
||||
accessURL, err = cfg.URL().Read()
|
||||
return err == nil
|
||||
}, 15*time.Second, 25*time.Millisecond)
|
||||
// Verify that authentication was properly set in dev-mode.
|
||||
token, err := cfg.Session().Read()
|
||||
require.NoError(t, err)
|
||||
parsed, err := url.Parse(accessURL)
|
||||
require.NoError(t, err)
|
||||
client := codersdk.New(parsed)
|
||||
client.SessionToken = token
|
||||
_, err = client.User(ctx, "")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package cli
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func workspaceTunnel() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "tunnel",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
|
@ -4,12 +4,15 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/powersj/whatsthis/pkg/cloud"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
|
||||
"github.com/coder/coder/agent"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/peer"
|
||||
)
|
||||
|
||||
func workspaceAgent() *cobra.Command {
|
||||
|
@ -30,26 +33,28 @@ func workspaceAgent() *cobra.Command {
|
|||
client := codersdk.New(coderURL)
|
||||
sessionToken, exists := os.LookupEnv("CODER_TOKEN")
|
||||
if !exists {
|
||||
probe, err := cloud.New()
|
||||
// probe, err := cloud.New()
|
||||
// if err != nil {
|
||||
// return xerrors.Errorf("probe cloud: %w", err)
|
||||
// }
|
||||
// if !probe.Detected {
|
||||
// return xerrors.Errorf("no valid authentication method found; set \"CODER_TOKEN\"")
|
||||
// }
|
||||
// switch {
|
||||
// case probe.GCP():
|
||||
response, err := client.AuthWorkspaceGoogleInstanceIdentity(cmd.Context(), "", nil)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("probe cloud: %w", err)
|
||||
}
|
||||
if !probe.Detected {
|
||||
return xerrors.Errorf("no valid authentication method found; set \"CODER_TOKEN\"")
|
||||
}
|
||||
switch {
|
||||
case probe.GCP():
|
||||
response, err := client.AuthWorkspaceGoogleInstanceIdentity(cmd.Context(), "", nil)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("authenticate workspace with gcp: %w", err)
|
||||
}
|
||||
sessionToken = response.SessionToken
|
||||
default:
|
||||
return xerrors.Errorf("%q authentication not supported; set \"CODER_TOKEN\" instead", probe.Name)
|
||||
return xerrors.Errorf("authenticate workspace with gcp: %w", err)
|
||||
}
|
||||
sessionToken = response.SessionToken
|
||||
// default:
|
||||
// return xerrors.Errorf("%q authentication not supported; set \"CODER_TOKEN\" instead", probe.Name)
|
||||
// }
|
||||
}
|
||||
client.SessionToken = sessionToken
|
||||
closer := agent.New(client.ListenWorkspaceAgent, nil)
|
||||
closer := agent.New(client.ListenWorkspaceAgent, &peer.ConnOptions{
|
||||
Logger: slog.Make(sloghuman.Sink(cmd.OutOrStdout())),
|
||||
})
|
||||
<-cmd.Context().Done()
|
||||
return closer.Close()
|
||||
},
|
||||
|
|
|
@ -3,21 +3,27 @@ package cli
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/fatih/color"
|
||||
"github.com/google/uuid"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
func workspaceCreate() *cobra.Command {
|
||||
var (
|
||||
projectName string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "create <project> [name]",
|
||||
Use: "create <name>",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Short: "Create a workspace from a project",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := createClient(cmd)
|
||||
|
@ -29,37 +35,51 @@ func workspaceCreate() *cobra.Command {
|
|||
return err
|
||||
}
|
||||
|
||||
var name string
|
||||
if len(args) >= 2 {
|
||||
name = args[1]
|
||||
} else {
|
||||
name, err = prompt(cmd, &promptui.Prompt{
|
||||
Label: "What's your workspace's name?",
|
||||
Validate: func(s string) error {
|
||||
if s == "" {
|
||||
return xerrors.Errorf("You must provide a name!")
|
||||
}
|
||||
workspace, _ := client.WorkspaceByName(cmd.Context(), "", s)
|
||||
if workspace.ID.String() != uuid.Nil.String() {
|
||||
return xerrors.New("A workspace already exists with that name!")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
var project codersdk.Project
|
||||
if projectName == "" {
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Wrap.Render("Select a project:"))
|
||||
|
||||
projectNames := []string{}
|
||||
projectByName := map[string]codersdk.Project{}
|
||||
projects, err := client.ProjectsByOrganization(cmd.Context(), organization.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, project := range projects {
|
||||
projectNames = append(projectNames, project.Name)
|
||||
projectByName[project.Name] = project
|
||||
}
|
||||
sort.Slice(projectNames, func(i, j int) bool {
|
||||
return projectByName[projectNames[i]].WorkspaceOwnerCount > projectByName[projectNames[j]].WorkspaceOwnerCount
|
||||
})
|
||||
// Move the cursor up a single line for nicer display!
|
||||
option, err := cliui.Select(cmd, cliui.SelectOptions{
|
||||
Options: projectNames,
|
||||
HideSearch: true,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, promptui.ErrAbort) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
project = projectByName[option]
|
||||
} else {
|
||||
project, err = client.ProjectByName(cmd.Context(), organization.ID, projectName)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get project by name: %w", err)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Previewing project create...\n", caret)
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout())
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"Creating with the "+cliui.Styles.Field.Render(project.Name)+" project...")
|
||||
|
||||
project, err := client.ProjectByName(cmd.Context(), organization.ID, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
workspaceName := args[0]
|
||||
_, err = client.WorkspaceByName(cmd.Context(), "", workspaceName)
|
||||
if err == nil {
|
||||
return xerrors.Errorf("A workspace already exists named %q!", workspaceName)
|
||||
}
|
||||
|
||||
projectVersion, err := client.ProjectVersion(cmd.Context(), project.ActiveVersionID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -68,22 +88,46 @@ func workspaceCreate() *cobra.Command {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parameterValues, err := client.ProjectVersionParameters(cmd.Context(), projectVersion.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
printed := false
|
||||
parameters := make([]codersdk.CreateParameterRequest, 0)
|
||||
for _, parameterSchema := range parameterSchemas {
|
||||
if !parameterSchema.AllowOverrideSource {
|
||||
continue
|
||||
}
|
||||
if !printed {
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("This project has customizable parameters! These can be changed after create, but may have unintended side effects (like data loss).")+"\r\n")
|
||||
printed = true
|
||||
}
|
||||
|
||||
value, err := cliui.ParameterSchema(cmd, parameterSchema)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parameters = append(parameters, codersdk.CreateParameterRequest{
|
||||
Name: parameterSchema.Name,
|
||||
SourceValue: value,
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
DestinationScheme: parameterSchema.DefaultDestinationScheme,
|
||||
})
|
||||
}
|
||||
if printed {
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout())
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.FocusedPrompt.String()+"Previewing resources...")
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout())
|
||||
}
|
||||
resources, err := client.ProjectVersionResources(cmd.Context(), projectVersion.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = displayProjectImportInfo(cmd, parameterSchemas, parameterValues, resources)
|
||||
err = displayProjectVersionInfo(cmd, resources)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = prompt(cmd, &promptui.Prompt{
|
||||
Label: fmt.Sprintf("Create workspace %s?", color.HiCyanString(name)),
|
||||
Default: "y",
|
||||
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
|
||||
Text: fmt.Sprintf("Create workspace %s?", color.HiCyanString(workspaceName)),
|
||||
Default: "yes",
|
||||
IsConfirm: true,
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -93,39 +137,70 @@ func workspaceCreate() *cobra.Command {
|
|||
return err
|
||||
}
|
||||
|
||||
workspace, err := client.CreateWorkspace(cmd.Context(), "", coderd.CreateWorkspaceRequest{
|
||||
ProjectID: project.ID,
|
||||
Name: name,
|
||||
before := time.Now()
|
||||
workspace, err := client.CreateWorkspace(cmd.Context(), "", codersdk.CreateWorkspaceRequest{
|
||||
ProjectID: project.ID,
|
||||
Name: workspaceName,
|
||||
ParameterValues: parameters,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: projectVersion.ID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
_, err = cliui.Job(cmd, cliui.JobOptions{
|
||||
Title: "Building workspace...",
|
||||
Fetch: func() (codersdk.ProvisionerJob, error) {
|
||||
build, err := client.WorkspaceBuild(cmd.Context(), workspace.LatestBuild.ID)
|
||||
return build.Job, err
|
||||
},
|
||||
Cancel: func() error {
|
||||
return client.CancelWorkspaceBuild(cmd.Context(), workspace.LatestBuild.ID)
|
||||
},
|
||||
Logs: func() (<-chan codersdk.ProvisionerJobLog, error) {
|
||||
return client.WorkspaceBuildLogsAfter(cmd.Context(), workspace.LatestBuild.ID, before)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logs, err := client.WorkspaceBuildLogsAfter(cmd.Context(), version.ID, time.Time{})
|
||||
resources, err = client.WorkspaceResourcesByBuild(cmd.Context(), workspace.LatestBuild.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
log, ok := <-logs
|
||||
if !ok {
|
||||
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond, spinner.WithColor("fgGreen"))
|
||||
spin.Writer = cmd.OutOrStdout()
|
||||
spin.Suffix = " Waiting for agent to connect..."
|
||||
spin.Start()
|
||||
defer spin.Stop()
|
||||
for _, resource := range resources {
|
||||
if resource.Agent == nil {
|
||||
continue
|
||||
}
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-cmd.Context().Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
}
|
||||
resource, err := client.WorkspaceResource(cmd.Context(), resource.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resource.Agent.FirstConnectedAt == nil {
|
||||
continue
|
||||
}
|
||||
spin.Stop()
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe %s workspace has been created!\n\n", cliui.Styles.Keyword.Render(workspace.Name))
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Code.Render("coder ssh "+workspace.Name))
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout())
|
||||
break
|
||||
}
|
||||
_, _ = fmt.Printf("Terraform: %s\n", log.Output)
|
||||
}
|
||||
|
||||
// This command is WIP, and output will change!
|
||||
|
||||
_, _ = fmt.Printf("Created workspace! %s\n", name)
|
||||
return nil
|
||||
return err
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&projectName, "project", "p", "", "Specify a project name.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
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/provisioner/echo"
|
||||
"github.com/coder/coder/provisionersdk/proto"
|
||||
"github.com/coder/coder/pty/ptytest"
|
||||
)
|
||||
|
||||
func TestWorkspaceCreate(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_ = coderdtest.NewProvisionerDaemon(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
Provision: []*proto.Provision_Response{{
|
||||
Type: &proto.Provision_Response_Complete{
|
||||
Complete: &proto.Provision_Complete{
|
||||
Resources: []*proto.Resource{{
|
||||
Name: "example",
|
||||
Type: "aws_instance",
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
})
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
cmd, root := clitest.New(t, "workspaces", "create", project.Name)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
|
||||
pty := ptytest.New(t)
|
||||
cmd.SetIn(pty.Input())
|
||||
cmd.SetOut(pty.Output())
|
||||
closeChan := make(chan struct{})
|
||||
go func() {
|
||||
err := cmd.Execute()
|
||||
require.NoError(t, err)
|
||||
close(closeChan)
|
||||
}()
|
||||
|
||||
matches := []string{
|
||||
"name?", "workspace-name",
|
||||
"Create workspace", "y",
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
match := matches[i]
|
||||
value := matches[i+1]
|
||||
pty.ExpectMatch(match)
|
||||
pty.WriteLine(value)
|
||||
}
|
||||
pty.ExpectMatch("Create")
|
||||
<-closeChan
|
||||
})
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
func workspaceDelete() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "delete <workspace>",
|
||||
Aliases: []string{"rm"},
|
||||
ValidArgsFunction: validArgsWorkspaceName,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := createClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workspace, err := client.WorkspaceByName(cmd.Context(), "", args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
before := time.Now()
|
||||
build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
Transition: database.WorkspaceTransitionDelete,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cliui.Job(cmd, cliui.JobOptions{
|
||||
Title: "Deleting workspace...",
|
||||
Fetch: func() (codersdk.ProvisionerJob, error) {
|
||||
build, err := client.WorkspaceBuild(cmd.Context(), build.ID)
|
||||
return build.Job, err
|
||||
},
|
||||
Cancel: func() error {
|
||||
return client.CancelWorkspaceBuild(cmd.Context(), build.ID)
|
||||
},
|
||||
Logs: func() (<-chan codersdk.ProvisionerJobLog, error) {
|
||||
return client.WorkspaceBuildLogsAfter(cmd.Context(), build.ID, before)
|
||||
},
|
||||
})
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
)
|
||||
|
||||
func workspaceList() *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
|
||||
}
|
||||
start := time.Now()
|
||||
workspaces, err := client.WorkspacesByUser(cmd.Context(), "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(workspaces) == 0 {
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"No workspaces found! Create one:")
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout())
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Code.Render("coder workspaces create <name>"))
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout())
|
||||
return nil
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Workspaces found %s\n\n",
|
||||
caret,
|
||||
color.HiBlackString("[%dms]",
|
||||
time.Since(start).Milliseconds()))
|
||||
|
||||
writer := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 4, ' ', 0)
|
||||
_, _ = fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%s\n",
|
||||
color.HiBlackString("Workspace"),
|
||||
color.HiBlackString("Project"),
|
||||
color.HiBlackString("Status"),
|
||||
color.HiBlackString("Last Built"),
|
||||
color.HiBlackString("Outdated"))
|
||||
for _, workspace := range workspaces {
|
||||
_, _ = fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%+v\n",
|
||||
color.New(color.FgHiCyan).Sprint(workspace.Name),
|
||||
color.WhiteString(workspace.ProjectName),
|
||||
color.WhiteString(string(workspace.LatestBuild.Transition)),
|
||||
color.WhiteString(workspace.LatestBuild.Job.CompletedAt.Format("January 2, 2006")),
|
||||
workspace.Outdated)
|
||||
}
|
||||
return writer.Flush()
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,13 +1,44 @@
|
|||
package cli
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func workspaces() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "workspaces",
|
||||
Use: "workspaces",
|
||||
Aliases: []string{"ws"},
|
||||
}
|
||||
cmd.AddCommand(workspaceAgent())
|
||||
cmd.AddCommand(workspaceCreate())
|
||||
cmd.AddCommand(workspaceDelete())
|
||||
cmd.AddCommand(workspaceList())
|
||||
cmd.AddCommand(workspaceShow())
|
||||
cmd.AddCommand(workspaceStop())
|
||||
cmd.AddCommand(workspaceStart())
|
||||
cmd.AddCommand(workspaceSSH())
|
||||
cmd.AddCommand(workspaceUpdate())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func validArgsWorkspaceName(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
client, err := createClient(cmd)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
workspaces, err := client.WorkspacesByUser(cmd.Context(), "")
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
names := make([]string, 0)
|
||||
for _, workspace := range workspaces {
|
||||
if !strings.HasPrefix(workspace.Name, toComplete) {
|
||||
continue
|
||||
}
|
||||
names = append(names, workspace.Name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func workspaceShow() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "show",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := createClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workspace, err := client.WorkspaceByName(cmd.Context(), "", args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resources, err := client.WorkspaceResourcesByBuild(cmd.Context(), workspace.LatestBuild.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, resource := range resources {
|
||||
if resource.Agent == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
_, _ = fmt.Printf("Agent: %+v\n", resource.Agent)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
func workspaceStart() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "start <workspace>",
|
||||
ValidArgsFunction: validArgsWorkspaceName,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := createClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workspace, err := client.WorkspaceByName(cmd.Context(), "", args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
before := time.Now()
|
||||
build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cliui.Job(cmd, cliui.JobOptions{
|
||||
Title: "Starting workspace...",
|
||||
Fetch: func() (codersdk.ProvisionerJob, error) {
|
||||
build, err := client.WorkspaceBuild(cmd.Context(), build.ID)
|
||||
return build.Job, err
|
||||
},
|
||||
Cancel: func() error {
|
||||
return client.CancelWorkspaceBuild(cmd.Context(), build.ID)
|
||||
},
|
||||
Logs: func() (<-chan codersdk.ProvisionerJobLog, error) {
|
||||
return client.WorkspaceBuildLogsAfter(cmd.Context(), build.ID, before)
|
||||
},
|
||||
})
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
func workspaceStop() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "stop <workspace>",
|
||||
ValidArgsFunction: validArgsWorkspaceName,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := createClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workspace, err := client.WorkspaceByName(cmd.Context(), "", args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
before := time.Now()
|
||||
build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
Transition: database.WorkspaceTransitionStop,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cliui.Job(cmd, cliui.JobOptions{
|
||||
Title: "Stopping workspace...",
|
||||
Fetch: func() (codersdk.ProvisionerJob, error) {
|
||||
build, err := client.WorkspaceBuild(cmd.Context(), build.ID)
|
||||
return build.Job, err
|
||||
},
|
||||
Cancel: func() error {
|
||||
return client.CancelWorkspaceBuild(cmd.Context(), build.ID)
|
||||
},
|
||||
Logs: func() (<-chan codersdk.ProvisionerJobLog, error) {
|
||||
return client.WorkspaceBuildLogsAfter(cmd.Context(), build.ID, before)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func workspaceUpdate() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "update",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := createClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workspace, err := client.WorkspaceByName(cmd.Context(), "", args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !workspace.Outdated {
|
||||
_, _ = fmt.Printf("Workspace isn't outdated!\n")
|
||||
return nil
|
||||
}
|
||||
project, err := client.Project(cmd.Context(), workspace.ProjectID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
before := time.Now()
|
||||
build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: workspace.LatestBuild.Transition,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logs, err := client.WorkspaceBuildLogsAfter(cmd.Context(), build.ID, before)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
log, ok := <-logs
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
_, _ = fmt.Printf("Output: %s\n", log.Output)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
func main() {
|
||||
root := &cobra.Command{
|
||||
Use: "cliui",
|
||||
Short: "Used for visually testing UI components for the CLI.",
|
||||
}
|
||||
|
||||
root.AddCommand(&cobra.Command{
|
||||
Use: "prompt",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
_, err := cliui.Prompt(cmd, cliui.PromptOptions{
|
||||
Text: "What is our " + cliui.Styles.Field.Render("company name") + "?",
|
||||
Default: "acme-corp",
|
||||
Validate: func(s string) error {
|
||||
if !strings.EqualFold(s, "coder") {
|
||||
return xerrors.New("Err... nope!")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if errors.Is(err, cliui.Canceled) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
|
||||
Text: "Do you want to accept?",
|
||||
Default: "yes",
|
||||
IsConfirm: true,
|
||||
})
|
||||
if errors.Is(err, cliui.Canceled) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
|
||||
Text: "Enter password",
|
||||
Secret: true,
|
||||
})
|
||||
return err
|
||||
},
|
||||
})
|
||||
|
||||
root.AddCommand(&cobra.Command{
|
||||
Use: "select",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
_, err := cliui.Select(cmd, cliui.SelectOptions{
|
||||
Options: []string{"Tomato", "Banana", "Onion", "Grape", "Lemon"},
|
||||
Size: 3,
|
||||
})
|
||||
return err
|
||||
},
|
||||
})
|
||||
|
||||
root.AddCommand(&cobra.Command{
|
||||
Use: "job",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
job := codersdk.ProvisionerJob{
|
||||
Status: codersdk.ProvisionerJobPending,
|
||||
CreatedAt: database.Now(),
|
||||
}
|
||||
go func() {
|
||||
time.Sleep(time.Second)
|
||||
if job.Status != codersdk.ProvisionerJobPending {
|
||||
return
|
||||
}
|
||||
started := database.Now()
|
||||
job.StartedAt = &started
|
||||
job.Status = codersdk.ProvisionerJobRunning
|
||||
time.Sleep(3 * time.Second)
|
||||
if job.Status != codersdk.ProvisionerJobRunning {
|
||||
return
|
||||
}
|
||||
completed := database.Now()
|
||||
job.CompletedAt = &completed
|
||||
job.Status = codersdk.ProvisionerJobSucceeded
|
||||
}()
|
||||
|
||||
_, err := cliui.Job(cmd, cliui.JobOptions{
|
||||
Fetch: func() (codersdk.ProvisionerJob, error) {
|
||||
return job, nil
|
||||
},
|
||||
Logs: func() (<-chan codersdk.ProvisionerJobLog, error) {
|
||||
logs := make(chan codersdk.ProvisionerJobLog)
|
||||
go func() {
|
||||
defer close(logs)
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
for {
|
||||
select {
|
||||
case <-cmd.Context().Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
logs <- codersdk.ProvisionerJobLog{
|
||||
Output: "Some log",
|
||||
Level: database.LogLevelInfo,
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return logs, nil
|
||||
},
|
||||
Cancel: func() error {
|
||||
job.Status = codersdk.ProvisionerJobCanceling
|
||||
time.Sleep(time.Second)
|
||||
job.Status = codersdk.ProvisionerJobCanceled
|
||||
completed := database.Now()
|
||||
job.CompletedAt = &completed
|
||||
return nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
},
|
||||
})
|
||||
|
||||
err := root.Execute()
|
||||
if err != nil {
|
||||
_, _ = fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
"google.golang.org/api/idtoken"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/tunnel"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/database/databasefake"
|
||||
"github.com/coder/coder/provisioner/terraform"
|
||||
"github.com/coder/coder/provisionerd"
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
"github.com/coder/coder/provisionersdk/proto"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var rawParameters []string
|
||||
cmd := &cobra.Command{
|
||||
Use: "templater",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
parameters := make([]codersdk.CreateParameterRequest, 0)
|
||||
for _, parameter := range rawParameters {
|
||||
parts := strings.SplitN(parameter, "=", 2)
|
||||
parameters = append(parameters, codersdk.CreateParameterRequest{
|
||||
Name: parts[0],
|
||||
SourceValue: parts[1],
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
|
||||
})
|
||||
}
|
||||
return parse(cmd, parameters)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringArrayVarP(&rawParameters, "parameter", "p", []string{}, "Specify parameters to pass in a template.")
|
||||
err := cmd.Execute()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func parse(cmd *cobra.Command, parameters []codersdk.CreateParameterRequest) error {
|
||||
srv := httptest.NewUnstartedServer(nil)
|
||||
srv.Config.BaseContext = func(_ net.Listener) context.Context {
|
||||
return cmd.Context()
|
||||
}
|
||||
srv.Start()
|
||||
serverURL, err := url.Parse(srv.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accessURL, errCh, err := tunnel.New(cmd.Context(), srv.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
err := <-errCh
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
accessURLParsed, err := url.Parse(accessURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var closeWait func()
|
||||
validator, err := idtoken.NewValidator(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger := slog.Make(sloghuman.Sink(cmd.OutOrStdout()))
|
||||
srv.Config.Handler, closeWait = coderd.New(&coderd.Options{
|
||||
AccessURL: accessURLParsed,
|
||||
Logger: logger,
|
||||
Database: databasefake.New(),
|
||||
Pubsub: database.NewPubsubInMemory(),
|
||||
GoogleTokenValidator: validator,
|
||||
})
|
||||
|
||||
client := codersdk.New(serverURL)
|
||||
daemonClose, err := newProvisionerDaemon(cmd.Context(), client, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer daemonClose.Close()
|
||||
|
||||
created, err := client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
|
||||
Email: "templater@coder.com",
|
||||
Username: "templater",
|
||||
Organization: "templater",
|
||||
Password: "insecure",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
auth, err := client.LoginWithPassword(cmd.Context(), codersdk.LoginWithPasswordRequest{
|
||||
Email: "templater@coder.com",
|
||||
Password: "insecure",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client.SessionToken = auth.SessionToken
|
||||
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content, err := provisionersdk.Tar(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := client.Upload(cmd.Context(), codersdk.ContentTypeTar, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
before := time.Now()
|
||||
version, err := client.CreateProjectVersion(cmd.Context(), created.OrganizationID, codersdk.CreateProjectVersionRequest{
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: resp.Hash,
|
||||
Provisioner: database.ProvisionerTypeTerraform,
|
||||
ParameterValues: parameters,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logs, err := client.ProjectVersionLogsAfter(cmd.Context(), version.ID, before)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
log, ok := <-logs
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
_, _ = fmt.Printf("terraform (%s): %s\n", log.Level, log.Output)
|
||||
}
|
||||
version, err = client.ProjectVersion(cmd.Context(), version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if version.Job.Status != codersdk.ProvisionerJobSucceeded {
|
||||
return xerrors.Errorf("Job wasn't successful, it was %q. Check the logs!", version.Job.Status)
|
||||
}
|
||||
|
||||
_, err = client.ProjectVersionResources(cmd.Context(), version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := client.CreateProject(cmd.Context(), created.OrganizationID, codersdk.CreateProjectRequest{
|
||||
Name: "test",
|
||||
VersionID: version.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
workspace, err := client.CreateWorkspace(cmd.Context(), created.UserID, codersdk.CreateWorkspaceRequest{
|
||||
ProjectID: project.ID,
|
||||
Name: "example",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logs, err = client.WorkspaceBuildLogsAfter(cmd.Context(), workspace.LatestBuild.ID, before)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
log, ok := <-logs
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
_, _ = fmt.Printf("terraform (%s): %s\n", log.Level, log.Output)
|
||||
}
|
||||
|
||||
resources, err := client.WorkspaceResourcesByBuild(cmd.Context(), workspace.LatestBuild.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, resource := range resources {
|
||||
if resource.Agent == nil {
|
||||
continue
|
||||
}
|
||||
err = awaitAgent(cmd.Context(), client, resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: version.ID,
|
||||
Transition: database.WorkspaceTransitionDelete,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logs, err = client.WorkspaceBuildLogsAfter(cmd.Context(), build.ID, before)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
log, ok := <-logs
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
_, _ = fmt.Printf("terraform (%s): %s\n", log.Level, log.Output)
|
||||
}
|
||||
|
||||
_ = daemonClose.Close()
|
||||
srv.Close()
|
||||
closeWait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func awaitAgent(ctx context.Context, client *codersdk.Client, resource codersdk.WorkspaceResource) error {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
resource, err := client.WorkspaceResource(ctx, resource.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resource.Agent.FirstConnectedAt == nil {
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newProvisionerDaemon(ctx context.Context, client *codersdk.Client, logger slog.Logger) (io.Closer, error) {
|
||||
terraformClient, terraformServer := provisionersdk.TransportPipe()
|
||||
go func() {
|
||||
err := terraform.Serve(ctx, &terraform.ServeOptions{
|
||||
ServeOptions: &provisionersdk.ServeOptions{
|
||||
Listener: terraformServer,
|
||||
},
|
||||
Logger: logger,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
tempDir, err := ioutil.TempDir("", "provisionerd")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return provisionerd.New(client.ListenProvisionerDaemon, &provisionerd.Options{
|
||||
Logger: logger,
|
||||
PollInterval: 50 * time.Millisecond,
|
||||
UpdateInterval: 500 * time.Millisecond,
|
||||
Provisioners: provisionerd.Provisioners{
|
||||
string(database.ProvisionerTypeTerraform): proto.NewDRPCProvisionerClient(provisionersdk.Conn(terraformClient)),
|
||||
},
|
||||
WorkDirectory: tempDir,
|
||||
}), nil
|
||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"google.golang.org/api/idtoken"
|
||||
|
@ -17,10 +18,11 @@ import (
|
|||
|
||||
// Options are requires parameters for Coder to start.
|
||||
type Options struct {
|
||||
AccessURL *url.URL
|
||||
Logger slog.Logger
|
||||
Database database.Store
|
||||
Pubsub database.Pubsub
|
||||
AgentConnectionUpdateFrequency time.Duration
|
||||
AccessURL *url.URL
|
||||
Logger slog.Logger
|
||||
Database database.Store
|
||||
Pubsub database.Pubsub
|
||||
|
||||
GoogleTokenValidator *idtoken.Validator
|
||||
}
|
||||
|
@ -30,6 +32,9 @@ type Options struct {
|
|||
// A wait function is returned to handle awaiting closure of hijacked HTTP
|
||||
// requests.
|
||||
func New(options *Options) (http.Handler, func()) {
|
||||
if options.AgentConnectionUpdateFrequency == 0 {
|
||||
options.AgentConnectionUpdateFrequency = 3 * time.Second
|
||||
}
|
||||
api := &api{
|
||||
Options: options,
|
||||
}
|
||||
|
@ -75,9 +80,10 @@ func New(options *Options) (http.Handler, func()) {
|
|||
httpmw.ExtractOrganizationParam(options.Database),
|
||||
)
|
||||
r.Get("/", api.project)
|
||||
r.Delete("/", api.deleteProject)
|
||||
r.Route("/versions", func(r chi.Router) {
|
||||
r.Get("/", api.projectVersionsByProject)
|
||||
r.Patch("/versions", nil)
|
||||
r.Patch("/", api.patchActiveProjectVersion)
|
||||
r.Get("/{projectversionname}", api.projectVersionByName)
|
||||
})
|
||||
})
|
||||
|
@ -89,6 +95,7 @@ func New(options *Options) (http.Handler, func()) {
|
|||
)
|
||||
|
||||
r.Get("/", api.projectVersion)
|
||||
r.Patch("/cancel", api.patchCancelProjectVersion)
|
||||
r.Get("/schema", api.projectVersionSchema)
|
||||
r.Get("/parameters", api.projectVersionParameters)
|
||||
r.Get("/resources", api.projectVersionResources)
|
||||
|
@ -153,7 +160,6 @@ func New(options *Options) (http.Handler, func()) {
|
|||
r.Route("/builds", func(r chi.Router) {
|
||||
r.Get("/", api.workspaceBuilds)
|
||||
r.Post("/", api.postWorkspaceBuilds)
|
||||
r.Get("/latest", api.workspaceBuildLatest)
|
||||
r.Get("/{workspacebuildname}", api.workspaceBuildByName)
|
||||
})
|
||||
})
|
||||
|
@ -164,6 +170,7 @@ func New(options *Options) (http.Handler, func()) {
|
|||
httpmw.ExtractWorkspaceParam(options.Database),
|
||||
)
|
||||
r.Get("/", api.workspaceBuild)
|
||||
r.Patch("/cancel", api.patchCancelWorkspaceBuild)
|
||||
r.Get("/logs", api.workspaceBuildLogs)
|
||||
r.Get("/resources", api.workspaceBuildResources)
|
||||
})
|
||||
|
|
|
@ -84,10 +84,11 @@ func New(t *testing.T, options *Options) *codersdk.Client {
|
|||
var closeWait func()
|
||||
// We set the handler after server creation for the access URL.
|
||||
srv.Config.Handler, closeWait = coderd.New(&coderd.Options{
|
||||
AccessURL: serverURL,
|
||||
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
AgentConnectionUpdateFrequency: 25 * time.Millisecond,
|
||||
AccessURL: serverURL,
|
||||
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
|
||||
GoogleTokenValidator: options.GoogleTokenValidator,
|
||||
})
|
||||
|
@ -118,9 +119,10 @@ func NewProvisionerDaemon(t *testing.T, client *codersdk.Client) io.Closer {
|
|||
}()
|
||||
|
||||
closer := provisionerd.New(client.ListenProvisionerDaemon, &provisionerd.Options{
|
||||
Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug),
|
||||
PollInterval: 50 * time.Millisecond,
|
||||
UpdateInterval: 50 * time.Millisecond,
|
||||
Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug),
|
||||
PollInterval: 50 * time.Millisecond,
|
||||
UpdateInterval: 250 * time.Millisecond,
|
||||
ForceCancelInterval: 250 * time.Millisecond,
|
||||
Provisioners: provisionerd.Provisioners{
|
||||
string(database.ProvisionerTypeEcho): proto.NewDRPCProvisionerClient(provisionersdk.Conn(echoClient)),
|
||||
},
|
||||
|
@ -134,8 +136,8 @@ func NewProvisionerDaemon(t *testing.T, client *codersdk.Client) io.Closer {
|
|||
|
||||
// CreateFirstUser creates a user with preset credentials and authenticates
|
||||
// with the passed in codersdk client.
|
||||
func CreateFirstUser(t *testing.T, client *codersdk.Client) coderd.CreateFirstUserResponse {
|
||||
req := coderd.CreateFirstUserRequest{
|
||||
func CreateFirstUser(t *testing.T, client *codersdk.Client) codersdk.CreateFirstUserResponse {
|
||||
req := codersdk.CreateFirstUserRequest{
|
||||
Email: "testuser@coder.com",
|
||||
Username: "testuser",
|
||||
Password: "testpass",
|
||||
|
@ -144,7 +146,7 @@ func CreateFirstUser(t *testing.T, client *codersdk.Client) coderd.CreateFirstUs
|
|||
resp, err := client.CreateFirstUser(context.Background(), req)
|
||||
require.NoError(t, err)
|
||||
|
||||
login, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
|
||||
login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
})
|
||||
|
@ -155,7 +157,7 @@ func CreateFirstUser(t *testing.T, client *codersdk.Client) coderd.CreateFirstUs
|
|||
|
||||
// CreateAnotherUser creates and authenticates a new user.
|
||||
func CreateAnotherUser(t *testing.T, client *codersdk.Client, organization string) *codersdk.Client {
|
||||
req := coderd.CreateUserRequest{
|
||||
req := codersdk.CreateUserRequest{
|
||||
Email: namesgenerator.GetRandomName(1) + "@coder.com",
|
||||
Username: randomUsername(),
|
||||
Password: "testpass",
|
||||
|
@ -164,7 +166,7 @@ func CreateAnotherUser(t *testing.T, client *codersdk.Client, organization strin
|
|||
_, err := client.CreateUser(context.Background(), req)
|
||||
require.NoError(t, err)
|
||||
|
||||
login, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
|
||||
login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
})
|
||||
|
@ -178,12 +180,12 @@ func CreateAnotherUser(t *testing.T, client *codersdk.Client, organization strin
|
|||
// CreateProjectVersion creates a project import provisioner job
|
||||
// with the responses provided. It uses the "echo" provisioner for compatibility
|
||||
// with testing.
|
||||
func CreateProjectVersion(t *testing.T, client *codersdk.Client, organization string, res *echo.Responses) coderd.ProjectVersion {
|
||||
func CreateProjectVersion(t *testing.T, client *codersdk.Client, organization string, res *echo.Responses) codersdk.ProjectVersion {
|
||||
data, err := echo.Tar(res)
|
||||
require.NoError(t, err)
|
||||
file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data)
|
||||
require.NoError(t, err)
|
||||
projectVersion, err := client.CreateProjectVersion(context.Background(), organization, coderd.CreateProjectVersionRequest{
|
||||
projectVersion, err := client.CreateProjectVersion(context.Background(), organization, codersdk.CreateProjectVersionRequest{
|
||||
StorageSource: file.Hash,
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
|
@ -194,8 +196,8 @@ func CreateProjectVersion(t *testing.T, client *codersdk.Client, organization st
|
|||
|
||||
// CreateProject creates a project with the "echo" provisioner for
|
||||
// compatibility with testing. The name assigned is randomly generated.
|
||||
func CreateProject(t *testing.T, client *codersdk.Client, organization string, version uuid.UUID) coderd.Project {
|
||||
project, err := client.CreateProject(context.Background(), organization, coderd.CreateProjectRequest{
|
||||
func CreateProject(t *testing.T, client *codersdk.Client, organization string, version uuid.UUID) codersdk.Project {
|
||||
project, err := client.CreateProject(context.Background(), organization, codersdk.CreateProjectRequest{
|
||||
Name: randomUsername(),
|
||||
VersionID: version,
|
||||
})
|
||||
|
@ -204,8 +206,8 @@ func CreateProject(t *testing.T, client *codersdk.Client, organization string, v
|
|||
}
|
||||
|
||||
// AwaitProjectImportJob awaits for an import job to reach completed status.
|
||||
func AwaitProjectVersionJob(t *testing.T, client *codersdk.Client, version uuid.UUID) coderd.ProjectVersion {
|
||||
var projectVersion coderd.ProjectVersion
|
||||
func AwaitProjectVersionJob(t *testing.T, client *codersdk.Client, version uuid.UUID) codersdk.ProjectVersion {
|
||||
var projectVersion codersdk.ProjectVersion
|
||||
require.Eventually(t, func() bool {
|
||||
var err error
|
||||
projectVersion, err = client.ProjectVersion(context.Background(), version)
|
||||
|
@ -216,8 +218,8 @@ func AwaitProjectVersionJob(t *testing.T, client *codersdk.Client, version uuid.
|
|||
}
|
||||
|
||||
// AwaitWorkspaceBuildJob waits for a workspace provision job to reach completed status.
|
||||
func AwaitWorkspaceBuildJob(t *testing.T, client *codersdk.Client, build uuid.UUID) coderd.WorkspaceBuild {
|
||||
var workspaceBuild coderd.WorkspaceBuild
|
||||
func AwaitWorkspaceBuildJob(t *testing.T, client *codersdk.Client, build uuid.UUID) codersdk.WorkspaceBuild {
|
||||
var workspaceBuild codersdk.WorkspaceBuild
|
||||
require.Eventually(t, func() bool {
|
||||
var err error
|
||||
workspaceBuild, err = client.WorkspaceBuild(context.Background(), build)
|
||||
|
@ -228,8 +230,8 @@ func AwaitWorkspaceBuildJob(t *testing.T, client *codersdk.Client, build uuid.UU
|
|||
}
|
||||
|
||||
// AwaitWorkspaceAgents waits for all resources with agents to be connected.
|
||||
func AwaitWorkspaceAgents(t *testing.T, client *codersdk.Client, build uuid.UUID) []coderd.WorkspaceResource {
|
||||
var resources []coderd.WorkspaceResource
|
||||
func AwaitWorkspaceAgents(t *testing.T, client *codersdk.Client, build uuid.UUID) []codersdk.WorkspaceResource {
|
||||
var resources []codersdk.WorkspaceResource
|
||||
require.Eventually(t, func() bool {
|
||||
var err error
|
||||
resources, err = client.WorkspaceResourcesByBuild(context.Background(), build)
|
||||
|
@ -238,7 +240,7 @@ func AwaitWorkspaceAgents(t *testing.T, client *codersdk.Client, build uuid.UUID
|
|||
if resource.Agent == nil {
|
||||
continue
|
||||
}
|
||||
if resource.Agent.UpdatedAt.IsZero() {
|
||||
if resource.Agent.FirstConnectedAt == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -249,8 +251,8 @@ func AwaitWorkspaceAgents(t *testing.T, client *codersdk.Client, build uuid.UUID
|
|||
|
||||
// CreateWorkspace creates a workspace for the user and project provided.
|
||||
// A random name is generated for it.
|
||||
func CreateWorkspace(t *testing.T, client *codersdk.Client, user string, projectID uuid.UUID) coderd.Workspace {
|
||||
workspace, err := client.CreateWorkspace(context.Background(), user, coderd.CreateWorkspaceRequest{
|
||||
func CreateWorkspace(t *testing.T, client *codersdk.Client, user string, projectID uuid.UUID) codersdk.Workspace {
|
||||
workspace, err := client.CreateWorkspace(context.Background(), user, codersdk.CreateWorkspaceRequest{
|
||||
ProjectID: projectID,
|
||||
Name: randomUsername(),
|
||||
})
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
package coderdtest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/goleak"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
@ -26,12 +21,7 @@ func TestNew(t *testing.T) {
|
|||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
|
||||
coderdtest.AwaitWorkspaceAgents(t, client, build.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
|
||||
closer.Close()
|
||||
}
|
||||
|
|
|
@ -12,16 +12,12 @@ import (
|
|||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
"github.com/coder/coder/httpmw"
|
||||
)
|
||||
|
||||
// UploadResponse contains the hash to reference the uploaded file.
|
||||
type UploadResponse struct {
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
|
||||
func (api *api) postFile(rw http.ResponseWriter, r *http.Request) {
|
||||
apiKey := httpmw.APIKey(r)
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
|
@ -49,7 +45,7 @@ func (api *api) postFile(rw http.ResponseWriter, r *http.Request) {
|
|||
if err == nil {
|
||||
// The file already exists!
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(rw, r, UploadResponse{
|
||||
render.JSON(rw, r, codersdk.UploadResponse{
|
||||
Hash: file.Hash,
|
||||
})
|
||||
return
|
||||
|
@ -68,7 +64,7 @@ func (api *api) postFile(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
render.Status(r, http.StatusCreated)
|
||||
render.JSON(rw, r, UploadResponse{
|
||||
render.JSON(rw, r, codersdk.UploadResponse{
|
||||
Hash: file.Hash,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
|
@ -13,45 +12,12 @@ import (
|
|||
"github.com/moby/moby/pkg/namesgenerator"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
"github.com/coder/coder/httpmw"
|
||||
)
|
||||
|
||||
// Organization is the JSON representation of a Coder organization.
|
||||
type Organization struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required"`
|
||||
UpdatedAt time.Time `json:"updated_at" validate:"required"`
|
||||
}
|
||||
|
||||
// CreateProjectVersionRequest enables callers to create a new Project Version.
|
||||
type CreateProjectVersionRequest struct {
|
||||
// ProjectID optionally associates a version with a project.
|
||||
ProjectID *uuid.UUID `json:"project_id"`
|
||||
|
||||
StorageMethod database.ProvisionerStorageMethod `json:"storage_method" validate:"oneof=file,required"`
|
||||
StorageSource string `json:"storage_source" validate:"required"`
|
||||
Provisioner database.ProvisionerType `json:"provisioner" validate:"oneof=terraform echo,required"`
|
||||
// ParameterValues allows for additional parameters to be provided
|
||||
// during the dry-run provision stage.
|
||||
ParameterValues []CreateParameterRequest `json:"parameter_values"`
|
||||
}
|
||||
|
||||
// CreateProjectRequest provides options when creating a project.
|
||||
type CreateProjectRequest struct {
|
||||
Name string `json:"name" validate:"username,required"`
|
||||
|
||||
// VersionID is an in-progress or completed job to use as
|
||||
// an initial version of the project.
|
||||
//
|
||||
// This is required on creation to enable a user-flow of validating a
|
||||
// project works. There is no reason the data-model cannot support
|
||||
// empty projects, but it doesn't make sense for users.
|
||||
VersionID uuid.UUID `json:"project_version_id" validate:"required"`
|
||||
}
|
||||
|
||||
func (*api) organization(rw http.ResponseWriter, r *http.Request) {
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
render.Status(r, http.StatusOK)
|
||||
|
@ -80,12 +46,12 @@ func (api *api) provisionerDaemonsByOrganization(rw http.ResponseWriter, r *http
|
|||
func (api *api) postProjectVersionsByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
apiKey := httpmw.APIKey(r)
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
var req CreateProjectVersionRequest
|
||||
var req codersdk.CreateProjectVersionRequest
|
||||
if !httpapi.Read(rw, r, &req) {
|
||||
return
|
||||
}
|
||||
if req.ProjectID != nil {
|
||||
_, err := api.Database.GetProjectByID(r.Context(), *req.ProjectID)
|
||||
if req.ProjectID != uuid.Nil {
|
||||
_, err := api.Database.GetProjectByID(r.Context(), req.ProjectID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: "project does not exist",
|
||||
|
@ -152,9 +118,9 @@ func (api *api) postProjectVersionsByOrganization(rw http.ResponseWriter, r *htt
|
|||
}
|
||||
|
||||
var projectID uuid.NullUUID
|
||||
if req.ProjectID != nil {
|
||||
if req.ProjectID != uuid.Nil {
|
||||
projectID = uuid.NullUUID{
|
||||
UUID: *req.ProjectID,
|
||||
UUID: req.ProjectID,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
@ -187,7 +153,7 @@ func (api *api) postProjectVersionsByOrganization(rw http.ResponseWriter, r *htt
|
|||
|
||||
// Create a new project in an organization.
|
||||
func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
var createProject CreateProjectRequest
|
||||
var createProject codersdk.CreateProjectRequest
|
||||
if !httpapi.Read(rw, r, &createProject) {
|
||||
return
|
||||
}
|
||||
|
@ -232,7 +198,7 @@ func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Reque
|
|||
return
|
||||
}
|
||||
|
||||
var project Project
|
||||
var project codersdk.Project
|
||||
err = api.Database.InTx(func(db database.Store) error {
|
||||
dbProject, err := db.InsertProject(r.Context(), database.InsertProjectParams{
|
||||
ID: uuid.New(),
|
||||
|
@ -256,6 +222,22 @@ func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Reque
|
|||
if err != nil {
|
||||
return xerrors.Errorf("insert project version: %s", err)
|
||||
}
|
||||
for _, parameterValue := range createProject.ParameterValues {
|
||||
_, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
|
||||
ID: uuid.New(),
|
||||
Name: parameterValue.Name,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
Scope: database.ParameterScopeProject,
|
||||
ScopeID: dbProject.ID.String(),
|
||||
SourceScheme: parameterValue.SourceScheme,
|
||||
SourceValue: parameterValue.SourceValue,
|
||||
DestinationScheme: parameterValue.DestinationScheme,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert parameter value: %w", err)
|
||||
}
|
||||
}
|
||||
project = convertProject(dbProject, 0)
|
||||
return nil
|
||||
})
|
||||
|
@ -272,7 +254,9 @@ func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Reque
|
|||
|
||||
func (api *api) projectsByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
projects, err := api.Database.GetProjectsByOrganization(r.Context(), organization.ID)
|
||||
projects, err := api.Database.GetProjectsByOrganization(r.Context(), database.GetProjectsByOrganizationParams{
|
||||
OrganizationID: organization.ID,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
|
@ -338,8 +322,8 @@ func (api *api) projectByOrganizationAndName(rw http.ResponseWriter, r *http.Req
|
|||
}
|
||||
|
||||
// convertOrganization consumes the database representation and outputs an API friendly representation.
|
||||
func convertOrganization(organization database.Organization) Organization {
|
||||
return Organization{
|
||||
func convertOrganization(organization database.Organization) codersdk.Organization {
|
||||
return codersdk.Organization{
|
||||
ID: organization.ID,
|
||||
Name: organization.Name,
|
||||
CreatedAt: organization.CreatedAt,
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
|
@ -40,8 +39,8 @@ func TestPostProjectVersionsByOrganization(t *testing.T) {
|
|||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
projectID := uuid.New()
|
||||
_, err := client.CreateProjectVersion(context.Background(), user.OrganizationID, coderd.CreateProjectVersionRequest{
|
||||
ProjectID: &projectID,
|
||||
_, err := client.CreateProjectVersion(context.Background(), user.OrganizationID, codersdk.CreateProjectVersionRequest{
|
||||
ProjectID: projectID,
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: "hash",
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
|
@ -55,7 +54,7 @@ func TestPostProjectVersionsByOrganization(t *testing.T) {
|
|||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateProjectVersion(context.Background(), user.OrganizationID, coderd.CreateProjectVersionRequest{
|
||||
_, err := client.CreateProjectVersion(context.Background(), user.OrganizationID, codersdk.CreateProjectVersionRequest{
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: "hash",
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
|
@ -77,11 +76,11 @@ func TestPostProjectVersionsByOrganization(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data)
|
||||
require.NoError(t, err)
|
||||
_, err = client.CreateProjectVersion(context.Background(), user.OrganizationID, coderd.CreateProjectVersionRequest{
|
||||
_, err = client.CreateProjectVersion(context.Background(), user.OrganizationID, codersdk.CreateProjectVersionRequest{
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: file.Hash,
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
ParameterValues: []coderd.CreateParameterRequest{{
|
||||
ParameterValues: []codersdk.CreateParameterRequest{{
|
||||
Name: "example",
|
||||
SourceValue: "value",
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
|
@ -108,7 +107,7 @@ func TestPostProjectsByOrganization(t *testing.T) {
|
|||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
_, err := client.CreateProject(context.Background(), user.OrganizationID, coderd.CreateProjectRequest{
|
||||
_, err := client.CreateProject(context.Background(), user.OrganizationID, codersdk.CreateProjectRequest{
|
||||
Name: project.Name,
|
||||
VersionID: version.ID,
|
||||
})
|
||||
|
@ -121,7 +120,7 @@ func TestPostProjectsByOrganization(t *testing.T) {
|
|||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateProject(context.Background(), user.OrganizationID, coderd.CreateProjectRequest{
|
||||
_, err := client.CreateProject(context.Background(), user.OrganizationID, codersdk.CreateProjectRequest{
|
||||
Name: "test",
|
||||
VersionID: uuid.New(),
|
||||
})
|
||||
|
@ -153,6 +152,17 @@ func TestProjectsByOrganization(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.Len(t, projects, 1)
|
||||
})
|
||||
t.Run("ListMultiple", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
projects, err := client.ProjectsByOrganization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, projects, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProjectByOrganizationAndName(t *testing.T) {
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package parameter
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Contains parses possible values for a conditional.
|
||||
func Contains(condition string) ([]string, bool, error) {
|
||||
if condition == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
expression, diags := hclsyntax.ParseExpression([]byte(condition), "", hcl.InitialPos)
|
||||
if len(diags) > 0 {
|
||||
return nil, false, xerrors.Errorf("parse condition: %s", diags.Error())
|
||||
}
|
||||
functionCallExpression, valid := expression.(*hclsyntax.FunctionCallExpr)
|
||||
if !valid {
|
||||
return nil, false, nil
|
||||
}
|
||||
if functionCallExpression.Name != "contains" {
|
||||
return nil, false, nil
|
||||
}
|
||||
if len(functionCallExpression.Args) < 2 {
|
||||
return nil, false, nil
|
||||
}
|
||||
value, diags := functionCallExpression.Args[0].Value(&hcl.EvalContext{})
|
||||
if len(diags) > 0 {
|
||||
return nil, false, xerrors.Errorf("parse value: %s", diags.Error())
|
||||
}
|
||||
possible := make([]string, 0)
|
||||
for _, subValue := range value.AsValueSlice() {
|
||||
if subValue.Type().FriendlyName() != "string" {
|
||||
continue
|
||||
}
|
||||
possible = append(possible, subValue.AsString())
|
||||
}
|
||||
sort.Strings(possible)
|
||||
return possible, true, nil
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package parameter_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/parameter"
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Contains", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
values, valid, err := parameter.Contains(`contains(["us-east1-a", "us-central1-a"], var.region)`)
|
||||
require.NoError(t, err)
|
||||
require.True(t, valid)
|
||||
require.Len(t, values, 2)
|
||||
})
|
||||
}
|
|
@ -5,47 +5,18 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
)
|
||||
|
||||
type ParameterScope string
|
||||
|
||||
const (
|
||||
ParameterOrganization ParameterScope = "organization"
|
||||
ParameterProject ParameterScope = "project"
|
||||
ParameterUser ParameterScope = "user"
|
||||
ParameterWorkspace ParameterScope = "workspace"
|
||||
)
|
||||
|
||||
// Parameter represents a set value for the scope.
|
||||
type Parameter struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
Scope ParameterScope `db:"scope" json:"scope"`
|
||||
ScopeID string `db:"scope_id" json:"scope_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
SourceScheme database.ParameterSourceScheme `db:"source_scheme" json:"source_scheme"`
|
||||
DestinationScheme database.ParameterDestinationScheme `db:"destination_scheme" json:"destination_scheme"`
|
||||
}
|
||||
|
||||
// CreateParameterRequest is used to create a new parameter value for a scope.
|
||||
type CreateParameterRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
SourceValue string `json:"source_value" validate:"required"`
|
||||
SourceScheme database.ParameterSourceScheme `json:"source_scheme" validate:"oneof=data,required"`
|
||||
DestinationScheme database.ParameterDestinationScheme `json:"destination_scheme" validate:"oneof=environment_variable provisioner_variable,required"`
|
||||
}
|
||||
|
||||
func (api *api) postParameter(rw http.ResponseWriter, r *http.Request) {
|
||||
var createRequest CreateParameterRequest
|
||||
var createRequest codersdk.CreateParameterRequest
|
||||
if !httpapi.Read(rw, r, &createRequest) {
|
||||
return
|
||||
}
|
||||
|
@ -110,7 +81,7 @@ func (api *api) parameters(rw http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
return
|
||||
}
|
||||
apiParameterValues := make([]Parameter, 0, len(parameterValues))
|
||||
apiParameterValues := make([]codersdk.Parameter, 0, len(parameterValues))
|
||||
for _, parameterValue := range parameterValues {
|
||||
apiParameterValues = append(apiParameterValues, convertParameterValue(parameterValue))
|
||||
}
|
||||
|
@ -154,12 +125,12 @@ func (api *api) deleteParameter(rw http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
}
|
||||
|
||||
func convertParameterValue(parameterValue database.ParameterValue) Parameter {
|
||||
return Parameter{
|
||||
func convertParameterValue(parameterValue database.ParameterValue) codersdk.Parameter {
|
||||
return codersdk.Parameter{
|
||||
ID: parameterValue.ID,
|
||||
CreatedAt: parameterValue.CreatedAt,
|
||||
UpdatedAt: parameterValue.UpdatedAt,
|
||||
Scope: ParameterScope(parameterValue.Scope),
|
||||
Scope: codersdk.ParameterScope(parameterValue.Scope),
|
||||
ScopeID: parameterValue.ScopeID,
|
||||
Name: parameterValue.Name,
|
||||
SourceScheme: parameterValue.SourceScheme,
|
||||
|
@ -170,13 +141,13 @@ func convertParameterValue(parameterValue database.ParameterValue) Parameter {
|
|||
func readScopeAndID(rw http.ResponseWriter, r *http.Request) (database.ParameterScope, string, bool) {
|
||||
var scope database.ParameterScope
|
||||
switch chi.URLParam(r, "scope") {
|
||||
case string(ParameterOrganization):
|
||||
case string(codersdk.ParameterOrganization):
|
||||
scope = database.ParameterScopeOrganization
|
||||
case string(ParameterProject):
|
||||
case string(codersdk.ParameterProject):
|
||||
scope = database.ParameterScopeProject
|
||||
case string(ParameterUser):
|
||||
case string(codersdk.ParameterUser):
|
||||
scope = database.ParameterScopeUser
|
||||
case string(ParameterWorkspace):
|
||||
case string(codersdk.ParameterWorkspace):
|
||||
scope = database.ParameterScopeWorkspace
|
||||
default:
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
|
@ -19,7 +18,7 @@ func TestPostParameter(t *testing.T) {
|
|||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateParameter(context.Background(), coderd.ParameterScope("something"), user.OrganizationID, coderd.CreateParameterRequest{
|
||||
_, err := client.CreateParameter(context.Background(), codersdk.ParameterScope("something"), user.OrganizationID, codersdk.CreateParameterRequest{
|
||||
Name: "example",
|
||||
SourceValue: "tomato",
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
|
@ -34,7 +33,7 @@ func TestPostParameter(t *testing.T) {
|
|||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateParameter(context.Background(), coderd.ParameterOrganization, user.OrganizationID, coderd.CreateParameterRequest{
|
||||
_, err := client.CreateParameter(context.Background(), codersdk.ParameterOrganization, user.OrganizationID, codersdk.CreateParameterRequest{
|
||||
Name: "example",
|
||||
SourceValue: "tomato",
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
|
@ -47,7 +46,7 @@ func TestPostParameter(t *testing.T) {
|
|||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateParameter(context.Background(), coderd.ParameterOrganization, user.OrganizationID, coderd.CreateParameterRequest{
|
||||
_, err := client.CreateParameter(context.Background(), codersdk.ParameterOrganization, user.OrganizationID, codersdk.CreateParameterRequest{
|
||||
Name: "example",
|
||||
SourceValue: "tomato",
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
|
@ -55,7 +54,7 @@ func TestPostParameter(t *testing.T) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.CreateParameter(context.Background(), coderd.ParameterOrganization, user.OrganizationID, coderd.CreateParameterRequest{
|
||||
_, err = client.CreateParameter(context.Background(), codersdk.ParameterOrganization, user.OrganizationID, codersdk.CreateParameterRequest{
|
||||
Name: "example",
|
||||
SourceValue: "tomato",
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
|
@ -73,21 +72,21 @@ func TestParameters(t *testing.T) {
|
|||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.Parameters(context.Background(), coderd.ParameterOrganization, user.OrganizationID)
|
||||
_, err := client.Parameters(context.Background(), codersdk.ParameterOrganization, user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateParameter(context.Background(), coderd.ParameterOrganization, user.OrganizationID, coderd.CreateParameterRequest{
|
||||
_, err := client.CreateParameter(context.Background(), codersdk.ParameterOrganization, user.OrganizationID, codersdk.CreateParameterRequest{
|
||||
Name: "example",
|
||||
SourceValue: "tomato",
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
params, err := client.Parameters(context.Background(), coderd.ParameterOrganization, user.OrganizationID)
|
||||
params, err := client.Parameters(context.Background(), codersdk.ParameterOrganization, user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, params, 1)
|
||||
})
|
||||
|
@ -99,7 +98,7 @@ func TestDeleteParameter(t *testing.T) {
|
|||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
err := client.DeleteParameter(context.Background(), coderd.ParameterOrganization, user.OrganizationID, "something")
|
||||
err := client.DeleteParameter(context.Background(), codersdk.ParameterOrganization, user.OrganizationID, "something")
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
|
@ -108,14 +107,14 @@ func TestDeleteParameter(t *testing.T) {
|
|||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
param, err := client.CreateParameter(context.Background(), coderd.ParameterOrganization, user.OrganizationID, coderd.CreateParameterRequest{
|
||||
param, err := client.CreateParameter(context.Background(), codersdk.ParameterOrganization, user.OrganizationID, codersdk.CreateParameterRequest{
|
||||
Name: "example",
|
||||
SourceValue: "tomato",
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = client.DeleteParameter(context.Background(), coderd.ParameterOrganization, user.OrganizationID, param.Name)
|
||||
err = client.DeleteParameter(context.Background(), codersdk.ParameterOrganization, user.OrganizationID, param.Name)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,31 +5,17 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
"github.com/coder/coder/httpmw"
|
||||
)
|
||||
|
||||
// Project is the JSON representation of a Coder project.
|
||||
// This type matches the database object for now, but is
|
||||
// abstracted for ease of change later on.
|
||||
type Project struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
OrganizationID string `json:"organization_id"`
|
||||
Name string `json:"name"`
|
||||
Provisioner database.ProvisionerType `json:"provisioner"`
|
||||
ActiveVersionID uuid.UUID `json:"active_version_id"`
|
||||
WorkspaceOwnerCount uint32 `json:"workspace_owner_count"`
|
||||
}
|
||||
|
||||
// Returns a single project.
|
||||
func (api *api) project(rw http.ResponseWriter, r *http.Request) {
|
||||
project := httpmw.ProjectParam(r)
|
||||
|
@ -52,6 +38,42 @@ func (api *api) project(rw http.ResponseWriter, r *http.Request) {
|
|||
render.JSON(rw, r, convertProject(project, count))
|
||||
}
|
||||
|
||||
func (api *api) deleteProject(rw http.ResponseWriter, r *http.Request) {
|
||||
project := httpmw.ProjectParam(r)
|
||||
|
||||
workspaces, err := api.Database.GetWorkspacesByProjectID(r.Context(), database.GetWorkspacesByProjectIDParams{
|
||||
ProjectID: project.ID,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspaces by project id: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if len(workspaces) > 0 {
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: "All workspaces must be deleted before a project can be removed.",
|
||||
})
|
||||
return
|
||||
}
|
||||
err = api.Database.UpdateProjectDeletedByID(r.Context(), database.UpdateProjectDeletedByIDParams{
|
||||
ID: project.ID,
|
||||
Deleted: true,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("update project deleted by id: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(rw, http.StatusOK, httpapi.Response{
|
||||
Message: "Project has been deleted!",
|
||||
})
|
||||
}
|
||||
|
||||
func (api *api) projectVersionsByProject(rw http.ResponseWriter, r *http.Request) {
|
||||
project := httpmw.ProjectParam(r)
|
||||
|
||||
|
@ -81,7 +103,7 @@ func (api *api) projectVersionsByProject(rw http.ResponseWriter, r *http.Request
|
|||
jobByID[job.ID.String()] = job
|
||||
}
|
||||
|
||||
apiVersion := make([]ProjectVersion, 0)
|
||||
apiVersion := make([]codersdk.ProjectVersion, 0)
|
||||
for _, version := range versions {
|
||||
job, exists := jobByID[version.JobID.String()]
|
||||
if !exists {
|
||||
|
@ -130,8 +152,48 @@ func (api *api) projectVersionByName(rw http.ResponseWriter, r *http.Request) {
|
|||
render.JSON(rw, r, convertProjectVersion(projectVersion, convertProvisionerJob(job)))
|
||||
}
|
||||
|
||||
func convertProjects(projects []database.Project, workspaceCounts []database.GetWorkspaceOwnerCountsByProjectIDsRow) []Project {
|
||||
apiProjects := make([]Project, 0, len(projects))
|
||||
func (api *api) patchActiveProjectVersion(rw http.ResponseWriter, r *http.Request) {
|
||||
var req codersdk.UpdateActiveProjectVersion
|
||||
if !httpapi.Read(rw, r, &req) {
|
||||
return
|
||||
}
|
||||
project := httpmw.ProjectParam(r)
|
||||
version, err := api.Database.GetProjectVersionByID(r.Context(), req.ID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: "project version not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get project version: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if version.ProjectID.UUID.String() != project.ID.String() {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: "The provided project version doesn't belong to the specified project.",
|
||||
})
|
||||
return
|
||||
}
|
||||
err = api.Database.UpdateProjectActiveVersionByID(r.Context(), database.UpdateProjectActiveVersionByIDParams{
|
||||
ID: project.ID,
|
||||
ActiveVersionID: req.ID,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("update active project version: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(rw, http.StatusOK, httpapi.Response{
|
||||
Message: "Updated the active project version!",
|
||||
})
|
||||
}
|
||||
|
||||
func convertProjects(projects []database.Project, workspaceCounts []database.GetWorkspaceOwnerCountsByProjectIDsRow) []codersdk.Project {
|
||||
apiProjects := make([]codersdk.Project, 0, len(projects))
|
||||
for _, project := range projects {
|
||||
found := false
|
||||
for _, workspaceCount := range workspaceCounts {
|
||||
|
@ -149,8 +211,8 @@ func convertProjects(projects []database.Project, workspaceCounts []database.Get
|
|||
return apiProjects
|
||||
}
|
||||
|
||||
func convertProject(project database.Project, workspaceOwnerCount uint32) Project {
|
||||
return Project{
|
||||
func convertProject(project database.Project, workspaceOwnerCount uint32) codersdk.Project {
|
||||
return codersdk.Project{
|
||||
ID: project.ID,
|
||||
CreatedAt: project.CreatedAt,
|
||||
UpdatedAt: project.UpdatedAt,
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
|
@ -25,6 +26,35 @@ func TestProject(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDeleteProject(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("NoWorkspaces", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
err := client.DeleteProject(context.Background(), project.ID)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Workspaces", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
err := client.DeleteProject(context.Background(), project.ID)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
|
||||
})
|
||||
}
|
||||
|
||||
func TestProjectVersionsByProject(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
|
@ -63,3 +93,47 @@ func TestProjectVersionByName(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchActiveProjectVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
err := client.UpdateActiveProjectVersion(context.Background(), project.ID, codersdk.UpdateActiveProjectVersion{
|
||||
ID: uuid.New(),
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("DoesNotBelong", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
version = coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
err := client.UpdateActiveProjectVersion(context.Background(), project.ID, codersdk.UpdateActiveProjectVersion{
|
||||
ID: version.ID,
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
err := client.UpdateActiveProjectVersion(context.Background(), project.ID, codersdk.UpdateActiveProjectVersion{
|
||||
ID: version.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,33 +5,16 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/coderd/parameter"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
"github.com/coder/coder/httpmw"
|
||||
)
|
||||
|
||||
// ProjectVersion represents a single version of a project.
|
||||
type ProjectVersion struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ProjectID *uuid.UUID `json:"project_id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Name string `json:"name"`
|
||||
Job ProvisionerJob `json:"job"`
|
||||
}
|
||||
|
||||
// ProjectVersionParameterSchema represents a parameter parsed from project version source.
|
||||
type ProjectVersionParameterSchema database.ParameterSchema
|
||||
|
||||
// ProjectVersionParameter represents a computed parameter value.
|
||||
type ProjectVersionParameter parameter.ComputedValue
|
||||
|
||||
func (api *api) projectVersion(rw http.ResponseWriter, r *http.Request) {
|
||||
projectVersion := httpmw.ProjectVersionParam(r)
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID)
|
||||
|
@ -45,6 +28,45 @@ func (api *api) projectVersion(rw http.ResponseWriter, r *http.Request) {
|
|||
render.JSON(rw, r, convertProjectVersion(projectVersion, convertProvisionerJob(job)))
|
||||
}
|
||||
|
||||
func (api *api) patchCancelProjectVersion(rw http.ResponseWriter, r *http.Request) {
|
||||
projectVersion := httpmw.ProjectVersionParam(r)
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if job.CompletedAt.Valid {
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: "Job has already completed!",
|
||||
})
|
||||
return
|
||||
}
|
||||
if job.CanceledAt.Valid {
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: "Job has already been marked as canceled!",
|
||||
})
|
||||
return
|
||||
}
|
||||
err = api.Database.UpdateProvisionerJobWithCancelByID(r.Context(), database.UpdateProvisionerJobWithCancelByIDParams{
|
||||
ID: job.ID,
|
||||
CanceledAt: sql.NullTime{
|
||||
Time: database.Now(),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("update provisioner job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(rw, http.StatusOK, httpapi.Response{
|
||||
Message: "Job has been marked as canceled...",
|
||||
})
|
||||
}
|
||||
|
||||
func (api *api) projectVersionSchema(rw http.ResponseWriter, r *http.Request) {
|
||||
projectVersion := httpmw.ProjectVersionParam(r)
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID)
|
||||
|
@ -138,8 +160,8 @@ func (api *api) projectVersionLogs(rw http.ResponseWriter, r *http.Request) {
|
|||
api.provisionerJobLogs(rw, r, job)
|
||||
}
|
||||
|
||||
func convertProjectVersion(version database.ProjectVersion, job ProvisionerJob) ProjectVersion {
|
||||
return ProjectVersion{
|
||||
func convertProjectVersion(version database.ProjectVersion, job codersdk.ProvisionerJob) codersdk.ProjectVersion {
|
||||
return codersdk.ProjectVersion{
|
||||
ID: version.ID,
|
||||
ProjectID: &version.ProjectID.UUID,
|
||||
CreatedAt: version.CreatedAt,
|
||||
|
|
|
@ -27,6 +27,38 @@ func TestProjectVersion(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPatchCancelProjectVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
Provision: []*proto.Provision_Response{{
|
||||
Type: &proto.Provision_Response_Log{
|
||||
Log: &proto.Log{},
|
||||
},
|
||||
}},
|
||||
})
|
||||
require.Eventually(t, func() bool {
|
||||
var err error
|
||||
version, err = client.ProjectVersion(context.Background(), version.ID)
|
||||
require.NoError(t, err)
|
||||
t.Logf("Status: %s", version.Job.Status)
|
||||
return version.Job.Status == codersdk.ProvisionerJobRunning
|
||||
}, 5*time.Second, 25*time.Millisecond)
|
||||
err := client.CancelProjectVersion(context.Background(), version.ID)
|
||||
require.NoError(t, err)
|
||||
require.Eventually(t, func() bool {
|
||||
var err error
|
||||
version, err = client.ProjectVersion(context.Background(), version.ID)
|
||||
require.NoError(t, err)
|
||||
// The echo provisioner doesn't respond to a shutdown request,
|
||||
// so the job cancel will time out and fail.
|
||||
return version.Job.Status == codersdk.ProvisionerJobFailed
|
||||
}, 5*time.Second, 25*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestProjectVersionSchema(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("ListRunning", func(t *testing.T) {
|
||||
|
|
|
@ -27,11 +27,10 @@ import (
|
|||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
"github.com/coder/coder/provisionerd/proto"
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
sdkproto "github.com/coder/coder/provisionersdk/proto"
|
||||
)
|
||||
|
||||
type ProvisionerDaemon database.ProvisionerDaemon
|
||||
|
||||
// Serves the provisioner daemon protobuf API over a WebSocket.
|
||||
func (api *api) provisionerDaemonsListen(rw http.ResponseWriter, r *http.Request) {
|
||||
api.websocketWaitGroup.Add(1)
|
||||
|
@ -266,6 +265,12 @@ func (server *provisionerdServer) UpdateJob(ctx context.Context, request *proto.
|
|||
if job.WorkerID.UUID.String() != server.ID.String() {
|
||||
return nil, xerrors.New("you don't own this job")
|
||||
}
|
||||
if job.CanceledAt.Valid {
|
||||
// Allows for graceful cancelation on the backend!
|
||||
return &proto.UpdateJobResponse{
|
||||
Canceled: true,
|
||||
}, nil
|
||||
}
|
||||
err = server.Database.UpdateProvisionerJobByID(ctx, database.UpdateProvisionerJobByIDParams{
|
||||
ID: parsedID,
|
||||
UpdatedAt: database.Now(),
|
||||
|
@ -358,8 +363,18 @@ func (server *provisionerdServer) UpdateJob(ctx context.Context, request *proto.
|
|||
}
|
||||
}
|
||||
|
||||
var projectID uuid.NullUUID
|
||||
if job.Type == database.ProvisionerJobTypeProjectVersionImport {
|
||||
projectVersion, err := server.Database.GetProjectVersionByJobID(ctx, job.ID)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get project version by job id: %w", err)
|
||||
}
|
||||
projectID = projectVersion.ProjectID
|
||||
}
|
||||
|
||||
parameters, err := parameter.Compute(ctx, server.Database, parameter.ComputeScope{
|
||||
ProjectImportJobID: job.ID,
|
||||
ProjectID: projectID,
|
||||
OrganizationID: job.OrganizationID,
|
||||
UserID: job.InitiatorID,
|
||||
}, nil)
|
||||
|
@ -454,14 +469,18 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
|
|||
database.WorkspaceTransitionStart: jobType.ProjectImport.StartResources,
|
||||
database.WorkspaceTransitionStop: jobType.ProjectImport.StopResources,
|
||||
} {
|
||||
for _, resource := range resources {
|
||||
addresses, err := provisionersdk.ResourceAddresses(resources)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("compute resource addresses: %w", err)
|
||||
}
|
||||
for index, resource := range resources {
|
||||
server.Logger.Info(ctx, "inserting project import job resource",
|
||||
slog.F("job_id", job.ID.String()),
|
||||
slog.F("resource_name", resource.Name),
|
||||
slog.F("resource_type", resource.Type),
|
||||
slog.F("transition", transition))
|
||||
|
||||
err = insertWorkspaceResource(ctx, server.Database, jobID, transition, resource)
|
||||
err = insertWorkspaceResource(ctx, server.Database, jobID, transition, resource, addresses[index])
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("insert resource: %w", err)
|
||||
}
|
||||
|
@ -515,13 +534,31 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
|
|||
if err != nil {
|
||||
return xerrors.Errorf("update workspace build: %w", err)
|
||||
}
|
||||
addresses, err := provisionersdk.ResourceAddresses(jobType.WorkspaceBuild.Resources)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("compute resource addresses: %w", err)
|
||||
}
|
||||
// This could be a bulk insert to improve performance.
|
||||
for _, protoResource := range jobType.WorkspaceBuild.Resources {
|
||||
err = insertWorkspaceResource(ctx, db, job.ID, workspaceBuild.Transition, protoResource)
|
||||
for index, protoResource := range jobType.WorkspaceBuild.Resources {
|
||||
err = insertWorkspaceResource(ctx, db, job.ID, workspaceBuild.Transition, protoResource, addresses[index])
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert provisioner job: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if workspaceBuild.Transition != database.WorkspaceTransitionDelete {
|
||||
// This is for deleting a workspace!
|
||||
return nil
|
||||
}
|
||||
|
||||
err = db.UpdateWorkspaceDeletedByID(ctx, database.UpdateWorkspaceDeletedByIDParams{
|
||||
ID: workspaceBuild.WorkspaceID,
|
||||
Deleted: true,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("update workspace deleted: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -535,12 +572,13 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
|
|||
return &proto.Empty{}, nil
|
||||
}
|
||||
|
||||
func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, protoResource *sdkproto.Resource) error {
|
||||
func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, protoResource *sdkproto.Resource, address string) error {
|
||||
resource, err := db.InsertWorkspaceResource(ctx, database.InsertWorkspaceResourceParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
JobID: jobID,
|
||||
Transition: transition,
|
||||
Address: address,
|
||||
Type: protoResource.Type,
|
||||
Name: protoResource.Name,
|
||||
AgentID: uuid.NullUUID{
|
||||
|
@ -581,6 +619,7 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
|
|||
_, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{
|
||||
ID: resource.AgentID.UUID,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
ResourceID: resource.ID,
|
||||
AuthToken: authToken,
|
||||
AuthInstanceID: instanceID,
|
||||
|
|
|
@ -15,39 +15,11 @@ import (
|
|||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
)
|
||||
|
||||
// ProvisionerJobStaus represents the at-time state of a job.
|
||||
type ProvisionerJobStatus string
|
||||
|
||||
const (
|
||||
ProvisionerJobPending ProvisionerJobStatus = "pending"
|
||||
ProvisionerJobRunning ProvisionerJobStatus = "running"
|
||||
ProvisionerJobSucceeded ProvisionerJobStatus = "succeeded"
|
||||
ProvisionerJobCancelled ProvisionerJobStatus = "canceled"
|
||||
ProvisionerJobFailed ProvisionerJobStatus = "failed"
|
||||
)
|
||||
|
||||
type ProvisionerJob struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Status ProvisionerJobStatus `json:"status"`
|
||||
WorkerID *uuid.UUID `json:"worker_id,omitempty"`
|
||||
}
|
||||
|
||||
type ProvisionerJobLog struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Source database.LogSource `json:"log_source"`
|
||||
Level database.LogLevel `json:"log_level"`
|
||||
Output string `json:"output"`
|
||||
}
|
||||
|
||||
// Returns provisioner logs based on query parameters.
|
||||
// The intended usage for a client to stream all logs (with JS API):
|
||||
// const timestamp = new Date().getTime();
|
||||
|
@ -220,7 +192,7 @@ func (api *api) provisionerJobResources(rw http.ResponseWriter, r *http.Request,
|
|||
})
|
||||
return
|
||||
}
|
||||
apiResources := make([]WorkspaceResource, 0)
|
||||
apiResources := make([]codersdk.WorkspaceResource, 0)
|
||||
for _, resource := range resources {
|
||||
if !resource.AgentID.Valid {
|
||||
apiResources = append(apiResources, convertWorkspaceResource(resource, nil))
|
||||
|
@ -233,7 +205,7 @@ func (api *api) provisionerJobResources(rw http.ResponseWriter, r *http.Request,
|
|||
})
|
||||
return
|
||||
}
|
||||
apiAgent, err := convertWorkspaceAgent(agent)
|
||||
apiAgent, err := convertWorkspaceAgent(agent, api.AgentConnectionUpdateFrequency)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("convert provisioner job agent: %s", err),
|
||||
|
@ -246,8 +218,8 @@ func (api *api) provisionerJobResources(rw http.ResponseWriter, r *http.Request,
|
|||
render.JSON(rw, r, apiResources)
|
||||
}
|
||||
|
||||
func convertProvisionerJobLog(provisionerJobLog database.ProvisionerJobLog) ProvisionerJobLog {
|
||||
return ProvisionerJobLog{
|
||||
func convertProvisionerJobLog(provisionerJobLog database.ProvisionerJobLog) codersdk.ProvisionerJobLog {
|
||||
return codersdk.ProvisionerJobLog{
|
||||
ID: provisionerJobLog.ID,
|
||||
CreatedAt: provisionerJobLog.CreatedAt,
|
||||
Source: provisionerJobLog.Source,
|
||||
|
@ -256,8 +228,8 @@ func convertProvisionerJobLog(provisionerJobLog database.ProvisionerJobLog) Prov
|
|||
}
|
||||
}
|
||||
|
||||
func convertProvisionerJob(provisionerJob database.ProvisionerJob) ProvisionerJob {
|
||||
job := ProvisionerJob{
|
||||
func convertProvisionerJob(provisionerJob database.ProvisionerJob) codersdk.ProvisionerJob {
|
||||
job := codersdk.ProvisionerJob{
|
||||
ID: provisionerJob.ID,
|
||||
CreatedAt: provisionerJob.CreatedAt,
|
||||
Error: provisionerJob.Error.String,
|
||||
|
@ -274,21 +246,25 @@ func convertProvisionerJob(provisionerJob database.ProvisionerJob) ProvisionerJo
|
|||
}
|
||||
|
||||
switch {
|
||||
case provisionerJob.CancelledAt.Valid:
|
||||
job.Status = ProvisionerJobCancelled
|
||||
case provisionerJob.CanceledAt.Valid:
|
||||
if provisionerJob.CompletedAt.Valid {
|
||||
job.Status = codersdk.ProvisionerJobCanceled
|
||||
} else {
|
||||
job.Status = codersdk.ProvisionerJobCanceling
|
||||
}
|
||||
case !provisionerJob.StartedAt.Valid:
|
||||
job.Status = ProvisionerJobPending
|
||||
job.Status = codersdk.ProvisionerJobPending
|
||||
case provisionerJob.CompletedAt.Valid:
|
||||
if job.Error == "" {
|
||||
job.Status = ProvisionerJobSucceeded
|
||||
job.Status = codersdk.ProvisionerJobSucceeded
|
||||
} else {
|
||||
job.Status = ProvisionerJobFailed
|
||||
job.Status = codersdk.ProvisionerJobFailed
|
||||
}
|
||||
case database.Now().Sub(provisionerJob.UpdatedAt) > 30*time.Second:
|
||||
job.Status = ProvisionerJobFailed
|
||||
job.Status = codersdk.ProvisionerJobFailed
|
||||
job.Error = "Worker failed to update job in time."
|
||||
default:
|
||||
job.Status = ProvisionerJobRunning
|
||||
job.Status = codersdk.ProvisionerJobRunning
|
||||
}
|
||||
|
||||
return job
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/provisioner/echo"
|
||||
|
@ -40,16 +39,11 @@ func TestProvisionerJobLogs(t *testing.T) {
|
|||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
before := time.Now().UTC()
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancelFunc)
|
||||
logs, err := client.WorkspaceBuildLogsAfter(ctx, build.ID, before)
|
||||
logs, err := client.WorkspaceBuildLogsAfter(ctx, workspace.LatestBuild.ID, before)
|
||||
require.NoError(t, err)
|
||||
log, ok := <-logs
|
||||
require.True(t, ok)
|
||||
|
@ -83,14 +77,9 @@ func TestProvisionerJobLogs(t *testing.T) {
|
|||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
before := database.Now()
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancelFunc)
|
||||
logs, err := client.WorkspaceBuildLogsAfter(ctx, build.ID, before)
|
||||
logs, err := client.WorkspaceBuildLogsAfter(ctx, workspace.LatestBuild.ID, before)
|
||||
require.NoError(t, err)
|
||||
log := <-logs
|
||||
require.Equal(t, "log-output", log.Output)
|
||||
|
@ -121,13 +110,8 @@ func TestProvisionerJobLogs(t *testing.T) {
|
|||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
|
||||
logs, err := client.WorkspaceBuildLogsBefore(context.Background(), build.ID, time.Now())
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
logs, err := client.WorkspaceBuildLogsBefore(context.Background(), workspace.LatestBuild.ID, time.Now())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, logs, 1)
|
||||
})
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// New creates a new tunnel pointing at the URL provided.
|
||||
// Once created, it returns the external hostname that will resolve to it.
|
||||
//
|
||||
// The tunnel will exit when the context provided is canceled.
|
||||
//
|
||||
// Upstream connection occurs async through Cloudflare, so the error channel
|
||||
// will only be executed if the tunnel has failed after numerous attempts.
|
||||
func New(ctx context.Context, url string) (string, <-chan error, error) {
|
||||
_ = os.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true")
|
||||
|
||||
httpTimeout := time.Second * 30
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSHandshakeTimeout: httpTimeout,
|
||||
ResponseHeaderTimeout: httpTimeout,
|
||||
},
|
||||
Timeout: httpTimeout,
|
||||
}
|
||||
|
||||
// Taken from:
|
||||
// https://github.com/cloudflare/cloudflared/blob/22cd8ceb8cf279afc1c412ae7f98308ffcfdd298/cmd/cloudflared/tunnel/quick_tunnel.go#L38
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.trycloudflare.com/tunnel", nil)
|
||||
if err != nil {
|
||||
return "", nil, xerrors.Errorf("create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", nil, xerrors.Errorf("request quick tunnel: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var data quickTunnelResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&data)
|
||||
if err != nil {
|
||||
return "", nil, xerrors.Errorf("decode: %w", err)
|
||||
}
|
||||
tunnelID, err := uuid.Parse(data.Result.ID)
|
||||
if err != nil {
|
||||
return "", nil, xerrors.Errorf("parse tunnel id: %w", err)
|
||||
}
|
||||
|
||||
credentials := connection.Credentials{
|
||||
AccountTag: data.Result.AccountTag,
|
||||
TunnelSecret: data.Result.Secret,
|
||||
TunnelID: tunnelID,
|
||||
}
|
||||
|
||||
namedTunnel := &connection.NamedTunnelProperties{
|
||||
Credentials: credentials,
|
||||
QuickTunnelUrl: data.Result.Hostname,
|
||||
}
|
||||
|
||||
set := flag.NewFlagSet("", 0)
|
||||
set.String("protocol", "", "")
|
||||
set.String("url", "", "")
|
||||
set.Int("retries", 5, "")
|
||||
appCtx := cli.NewContext(&cli.App{}, set, nil)
|
||||
appCtx.Context = ctx
|
||||
_ = appCtx.Set("url", url)
|
||||
_ = appCtx.Set("protocol", "quic")
|
||||
logger := zerolog.New(os.Stdout).Level(zerolog.Disabled)
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
err := tunnel.StartServer(appCtx, &cliutil.BuildInfo{}, namedTunnel, &logger, false)
|
||||
errCh <- err
|
||||
}()
|
||||
if !strings.HasPrefix(data.Result.Hostname, "https://") {
|
||||
data.Result.Hostname = "https://" + data.Result.Hostname
|
||||
}
|
||||
return data.Result.Hostname, errCh, nil
|
||||
}
|
||||
|
||||
type quickTunnelResponse struct {
|
||||
Success bool
|
||||
Result quickTunnel
|
||||
Errors []quickTunnelError
|
||||
}
|
||||
|
||||
type quickTunnelError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
type quickTunnel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Hostname string `json:"hostname"`
|
||||
AccountTag string `json:"account_tag"`
|
||||
Secret []byte `json:"secret"`
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package tunnel_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/tunnel"
|
||||
)
|
||||
|
||||
// The tunnel leaks a few goroutines that aren't impactful to production scenarios.
|
||||
// func TestMain(m *testing.M) {
|
||||
// goleak.VerifyTestMain(m)
|
||||
// }
|
||||
|
||||
func TestTunnel(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.Skip()
|
||||
return
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
url, _, err := tunnel.New(ctx, srv.URL)
|
||||
require.NoError(t, err)
|
||||
t.Log(url)
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
require.NoError(t, err)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
var dnsErr *net.DNSError
|
||||
// The name might take a bit to resolve!
|
||||
if xerrors.As(err, &dnsErr) {
|
||||
return false
|
||||
}
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
return res.StatusCode == http.StatusOK
|
||||
}, 5*time.Minute, 3*time.Second)
|
||||
}
|
301
coderd/users.go
301
coderd/users.go
|
@ -3,6 +3,7 @@ package coderd
|
|||
import (
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -11,69 +12,17 @@ import (
|
|||
"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"
|
||||
|
||||
"github.com/coder/coder/coderd/userpassword"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/cryptorand"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
"github.com/coder/coder/httpmw"
|
||||
)
|
||||
|
||||
// User represents a user in Coder.
|
||||
type User struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
Email string `json:"email" validate:"required"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
}
|
||||
|
||||
type CreateFirstUserRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Username string `json:"username" validate:"required,username"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
Organization string `json:"organization" validate:"required,username"`
|
||||
}
|
||||
|
||||
// CreateFirstUserResponse contains IDs for newly created user info.
|
||||
type CreateFirstUserResponse struct {
|
||||
UserID string `json:"user_id"`
|
||||
OrganizationID string `json:"organization_id"`
|
||||
}
|
||||
|
||||
type CreateUserRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Username string `json:"username" validate:"required,username"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
OrganizationID string `json:"organization_id" validate:"required"`
|
||||
}
|
||||
|
||||
// LoginWithPasswordRequest enables callers to authenticate with email and password.
|
||||
type LoginWithPasswordRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
// LoginWithPasswordResponse contains a session token for the newly authenticated user.
|
||||
type LoginWithPasswordResponse struct {
|
||||
SessionToken string `json:"session_token" validate:"required"`
|
||||
}
|
||||
|
||||
// GenerateAPIKeyResponse contains an API key for a user.
|
||||
type GenerateAPIKeyResponse struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type CreateOrganizationRequest struct {
|
||||
Name string `json:"name" validate:"required,username"`
|
||||
}
|
||||
|
||||
// CreateWorkspaceRequest provides options for creating a new workspace.
|
||||
type CreateWorkspaceRequest struct {
|
||||
ProjectID uuid.UUID `json:"project_id" validate:"required"`
|
||||
Name string `json:"name" validate:"username,required"`
|
||||
}
|
||||
|
||||
// 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())
|
||||
|
@ -96,7 +45,7 @@ func (api *api) firstUser(rw http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// Creates the initial user for a Coder deployment.
|
||||
func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) {
|
||||
var createUser CreateFirstUserRequest
|
||||
var createUser codersdk.CreateFirstUserRequest
|
||||
if !httpapi.Read(rw, r, &createUser) {
|
||||
return
|
||||
}
|
||||
|
@ -168,7 +117,7 @@ func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
render.Status(r, http.StatusCreated)
|
||||
render.JSON(rw, r, CreateFirstUserResponse{
|
||||
render.JSON(rw, r, codersdk.CreateFirstUserResponse{
|
||||
UserID: user.ID,
|
||||
OrganizationID: organization.ID,
|
||||
})
|
||||
|
@ -178,7 +127,7 @@ func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) {
|
|||
func (api *api) postUsers(rw http.ResponseWriter, r *http.Request) {
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
var createUser CreateUserRequest
|
||||
var createUser codersdk.CreateUserRequest
|
||||
if !httpapi.Read(rw, r, &createUser) {
|
||||
return
|
||||
}
|
||||
|
@ -299,7 +248,7 @@ func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
publicOrganizations := make([]Organization, 0, len(organizations))
|
||||
publicOrganizations := make([]codersdk.Organization, 0, len(organizations))
|
||||
for _, organization := range organizations {
|
||||
publicOrganizations = append(publicOrganizations, convertOrganization(organization))
|
||||
}
|
||||
|
@ -347,7 +296,7 @@ func (api *api) organizationByUserAndName(rw http.ResponseWriter, r *http.Reques
|
|||
|
||||
func (api *api) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
var req CreateOrganizationRequest
|
||||
var req codersdk.CreateOrganizationRequest
|
||||
if !httpapi.Read(rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
@ -401,7 +350,7 @@ func (api *api) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request)
|
|||
|
||||
// Authenticates the user with an email and password.
|
||||
func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) {
|
||||
var loginWithPassword LoginWithPasswordRequest
|
||||
var loginWithPassword codersdk.LoginWithPasswordRequest
|
||||
if !httpapi.Read(rw, r, &loginWithPassword) {
|
||||
return
|
||||
}
|
||||
|
@ -471,7 +420,7 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
|
||||
render.Status(r, http.StatusCreated)
|
||||
render.JSON(rw, r, LoginWithPasswordResponse{
|
||||
render.JSON(rw, r, codersdk.LoginWithPasswordResponse{
|
||||
SessionToken: sessionToken,
|
||||
})
|
||||
}
|
||||
|
@ -517,7 +466,7 @@ func (api *api) postAPIKey(rw http.ResponseWriter, r *http.Request) {
|
|||
generatedAPIKey := fmt.Sprintf("%s-%s", keyID, keySecret)
|
||||
|
||||
render.Status(r, http.StatusCreated)
|
||||
render.JSON(rw, r, GenerateAPIKeyResponse{Key: generatedAPIKey})
|
||||
render.JSON(rw, r, codersdk.GenerateAPIKeyResponse{Key: generatedAPIKey})
|
||||
}
|
||||
|
||||
// Clear the user's session cookie
|
||||
|
@ -536,7 +485,7 @@ func (*api) postLogout(rw http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// Create a new workspace for the currently authenticated user.
|
||||
func (api *api) postWorkspacesByUser(rw http.ResponseWriter, r *http.Request) {
|
||||
var createWorkspace CreateWorkspaceRequest
|
||||
var createWorkspace codersdk.CreateWorkspaceRequest
|
||||
if !httpapi.Read(rw, r, &createWorkspace) {
|
||||
return
|
||||
}
|
||||
|
@ -605,29 +554,126 @@ func (api *api) postWorkspacesByUser(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Workspaces are created without any versions.
|
||||
workspace, err = api.Database.InsertWorkspace(r.Context(), database.InsertWorkspaceParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
OwnerID: apiKey.UserID,
|
||||
ProjectID: project.ID,
|
||||
Name: createWorkspace.Name,
|
||||
projectVersion, err := api.Database.GetProjectVersionByID(r.Context(), project.ActiveVersionID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get project version: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
projectVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get project version job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
projectVersionJobStatus := convertProvisionerJob(projectVersionJob).Status
|
||||
switch projectVersionJobStatus {
|
||||
case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning:
|
||||
httpapi.Write(rw, http.StatusNotAcceptable, httpapi.Response{
|
||||
Message: fmt.Sprintf("The provided project version is %s. Wait for it to complete importing!", projectVersionJobStatus),
|
||||
})
|
||||
return
|
||||
case codersdk.ProvisionerJobFailed:
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: fmt.Sprintf("The provided project version %q has failed to import. You cannot create workspaces using it!", projectVersion.Name),
|
||||
})
|
||||
return
|
||||
case codersdk.ProvisionerJobCanceled:
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: "The provided project version was canceled during import. You cannot create workspaces using it!",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var provisionerJob database.ProvisionerJob
|
||||
var workspaceBuild database.WorkspaceBuild
|
||||
err = api.Database.InTx(func(db database.Store) error {
|
||||
workspaceBuildID := uuid.New()
|
||||
// Workspaces are created without any versions.
|
||||
workspace, err = db.InsertWorkspace(r.Context(), database.InsertWorkspaceParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
OwnerID: apiKey.UserID,
|
||||
ProjectID: project.ID,
|
||||
Name: createWorkspace.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert workspace: %w", err)
|
||||
}
|
||||
for _, parameterValue := range createWorkspace.ParameterValues {
|
||||
_, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
|
||||
ID: uuid.New(),
|
||||
Name: parameterValue.Name,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
Scope: database.ParameterScopeWorkspace,
|
||||
ScopeID: workspace.ID.String(),
|
||||
SourceScheme: parameterValue.SourceScheme,
|
||||
SourceValue: parameterValue.SourceValue,
|
||||
DestinationScheme: parameterValue.DestinationScheme,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert parameter value: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
input, err := json.Marshal(workspaceProvisionJob{
|
||||
WorkspaceBuildID: workspaceBuildID,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("marshal provision job: %w", err)
|
||||
}
|
||||
provisionerJob, err = db.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
InitiatorID: apiKey.UserID,
|
||||
OrganizationID: project.OrganizationID,
|
||||
Provisioner: project.Provisioner,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
StorageMethod: projectVersionJob.StorageMethod,
|
||||
StorageSource: projectVersionJob.StorageSource,
|
||||
Input: input,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert provisioner job: %w", err)
|
||||
}
|
||||
workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{
|
||||
ID: workspaceBuildID,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
WorkspaceID: workspace.ID,
|
||||
ProjectVersionID: projectVersion.ID,
|
||||
Name: namesgenerator.GetRandomName(1),
|
||||
Initiator: apiKey.UserID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
JobID: provisionerJob.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert workspace build: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("insert workspace: %s", err),
|
||||
Message: fmt.Sprintf("create workspace: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusCreated)
|
||||
render.JSON(rw, r, convertWorkspace(workspace))
|
||||
render.JSON(rw, r, convertWorkspace(workspace,
|
||||
convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(projectVersionJob)), project))
|
||||
}
|
||||
|
||||
func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
workspaces, err := api.Database.GetWorkspacesByUserID(r.Context(), user.ID)
|
||||
workspaces, err := api.Database.GetWorkspacesByUserID(r.Context(), database.GetWorkspacesByUserIDParams{
|
||||
OwnerID: user.ID,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
|
@ -637,9 +683,84 @@ func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
return
|
||||
}
|
||||
apiWorkspaces := make([]Workspace, 0, len(workspaces))
|
||||
workspaceIDs := make([]uuid.UUID, 0, len(workspaces))
|
||||
projectIDs := make([]uuid.UUID, 0, len(workspaces))
|
||||
for _, workspace := range workspaces {
|
||||
apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace))
|
||||
workspaceIDs = append(workspaceIDs, workspace.ID)
|
||||
projectIDs = append(projectIDs, workspace.ProjectID)
|
||||
}
|
||||
workspaceBuilds, err := api.Database.GetWorkspaceBuildsByWorkspaceIDsWithoutAfter(r.Context(), workspaceIDs)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace builds: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
projects, err := api.Database.GetProjectsByIDs(r.Context(), projectIDs)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get projects: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
jobIDs := make([]uuid.UUID, 0, len(workspaceBuilds))
|
||||
for _, build := range workspaceBuilds {
|
||||
jobIDs = append(jobIDs, build.JobID)
|
||||
}
|
||||
jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner jobs: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
buildByWorkspaceID := map[string]database.WorkspaceBuild{}
|
||||
for _, workspaceBuild := range workspaceBuilds {
|
||||
buildByWorkspaceID[workspaceBuild.WorkspaceID.String()] = workspaceBuild
|
||||
}
|
||||
projectByID := map[string]database.Project{}
|
||||
for _, project := range projects {
|
||||
projectByID[project.ID.String()] = project
|
||||
}
|
||||
jobByID := map[string]database.ProvisionerJob{}
|
||||
for _, job := range jobs {
|
||||
jobByID[job.ID.String()] = job
|
||||
}
|
||||
apiWorkspaces := make([]codersdk.Workspace, 0, len(workspaces))
|
||||
for _, workspace := range workspaces {
|
||||
build, exists := buildByWorkspaceID[workspace.ID.String()]
|
||||
if !exists {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("build not found for workspace %q", workspace.Name),
|
||||
})
|
||||
return
|
||||
}
|
||||
project, exists := projectByID[workspace.ProjectID.String()]
|
||||
if !exists {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("project not found for workspace %q", workspace.Name),
|
||||
})
|
||||
return
|
||||
}
|
||||
job, exists := jobByID[build.JobID.String()]
|
||||
if !exists {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("build job not found for workspace %q", workspace.Name),
|
||||
})
|
||||
return
|
||||
}
|
||||
apiWorkspaces = append(apiWorkspaces,
|
||||
convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), project))
|
||||
}
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(rw, r, apiWorkspaces)
|
||||
|
@ -664,9 +785,31 @@ func (api *api) workspaceByUserAndName(rw http.ResponseWriter, r *http.Request)
|
|||
})
|
||||
return
|
||||
}
|
||||
build, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace build: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), build.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
project, err := api.Database.GetProjectByID(r.Context(), workspace.ProjectID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get project: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(rw, r, convertWorkspace(workspace))
|
||||
render.JSON(rw, r, convertWorkspace(workspace,
|
||||
convertWorkspaceBuild(build, convertProvisionerJob(job)), project))
|
||||
}
|
||||
|
||||
// Generates a new ID and secret for an API key.
|
||||
|
@ -684,8 +827,8 @@ func generateAPIKeyIDSecret() (id string, secret string, err error) {
|
|||
return id, secret, nil
|
||||
}
|
||||
|
||||
func convertUser(user database.User) User {
|
||||
return User{
|
||||
func convertUser(user database.User) codersdk.User {
|
||||
return codersdk.User{
|
||||
ID: user.ID,
|
||||
Email: user.Email,
|
||||
CreatedAt: user.CreatedAt,
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/httpmw"
|
||||
|
@ -19,7 +18,7 @@ func TestFirstUser(t *testing.T) {
|
|||
t.Run("BadRequest", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_, err := client.CreateFirstUser(context.Background(), coderd.CreateFirstUserRequest{})
|
||||
_, err := client.CreateFirstUser(context.Background(), codersdk.CreateFirstUserRequest{})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
|
@ -27,7 +26,7 @@ func TestFirstUser(t *testing.T) {
|
|||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateFirstUser(context.Background(), coderd.CreateFirstUserRequest{
|
||||
_, err := client.CreateFirstUser(context.Background(), codersdk.CreateFirstUserRequest{
|
||||
Email: "some@email.com",
|
||||
Username: "exampleuser",
|
||||
Password: "password",
|
||||
|
@ -50,7 +49,7 @@ func TestPostLogin(t *testing.T) {
|
|||
t.Run("InvalidUser", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
|
||||
_, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
|
||||
Email: "my@email.org",
|
||||
Password: "password",
|
||||
})
|
||||
|
@ -62,7 +61,7 @@ func TestPostLogin(t *testing.T) {
|
|||
t.Run("BadPassword", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
req := coderd.CreateFirstUserRequest{
|
||||
req := codersdk.CreateFirstUserRequest{
|
||||
Email: "testuser@coder.com",
|
||||
Username: "testuser",
|
||||
Password: "testpass",
|
||||
|
@ -70,7 +69,7 @@ func TestPostLogin(t *testing.T) {
|
|||
}
|
||||
_, err := client.CreateFirstUser(context.Background(), req)
|
||||
require.NoError(t, err)
|
||||
_, err = client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
|
||||
_, err = client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
|
||||
Email: req.Email,
|
||||
Password: "badpass",
|
||||
})
|
||||
|
@ -82,7 +81,7 @@ func TestPostLogin(t *testing.T) {
|
|||
t.Run("Success", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
req := coderd.CreateFirstUserRequest{
|
||||
req := codersdk.CreateFirstUserRequest{
|
||||
Email: "testuser@coder.com",
|
||||
Username: "testuser",
|
||||
Password: "testpass",
|
||||
|
@ -90,7 +89,7 @@ func TestPostLogin(t *testing.T) {
|
|||
}
|
||||
_, err := client.CreateFirstUser(context.Background(), req)
|
||||
require.NoError(t, err)
|
||||
_, err = client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
|
||||
_, err = client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
})
|
||||
|
@ -130,7 +129,7 @@ func TestPostUsers(t *testing.T) {
|
|||
t.Run("NoAuth", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{})
|
||||
_, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
|
@ -140,7 +139,7 @@ func TestPostUsers(t *testing.T) {
|
|||
coderdtest.CreateFirstUser(t, client)
|
||||
me, err := client.User(context.Background(), "")
|
||||
require.NoError(t, err)
|
||||
_, err = client.CreateUser(context.Background(), coderd.CreateUserRequest{
|
||||
_, err = client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
||||
Email: me.Email,
|
||||
Username: me.Username,
|
||||
Password: "password",
|
||||
|
@ -155,7 +154,7 @@ func TestPostUsers(t *testing.T) {
|
|||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{
|
||||
_, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
||||
OrganizationID: "not-exists",
|
||||
Email: "another@user.org",
|
||||
Username: "someone-else",
|
||||
|
@ -171,12 +170,12 @@ func TestPostUsers(t *testing.T) {
|
|||
client := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, client)
|
||||
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
||||
org, err := other.CreateOrganization(context.Background(), "", coderd.CreateOrganizationRequest{
|
||||
org, err := other.CreateOrganization(context.Background(), "", codersdk.CreateOrganizationRequest{
|
||||
Name: "another",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.CreateUser(context.Background(), coderd.CreateUserRequest{
|
||||
_, err = client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
||||
Email: "some@domain.com",
|
||||
Username: "anotheruser",
|
||||
Password: "testing",
|
||||
|
@ -191,7 +190,7 @@ func TestPostUsers(t *testing.T) {
|
|||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{
|
||||
_, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
||||
OrganizationID: user.OrganizationID,
|
||||
Email: "another@user.org",
|
||||
Username: "someone-else",
|
||||
|
@ -236,7 +235,7 @@ func TestOrganizationByUserAndName(t *testing.T) {
|
|||
client := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, client)
|
||||
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
||||
org, err := other.CreateOrganization(context.Background(), "", coderd.CreateOrganizationRequest{
|
||||
org, err := other.CreateOrganization(context.Background(), "", codersdk.CreateOrganizationRequest{
|
||||
Name: "another",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
@ -265,7 +264,7 @@ func TestPostOrganizationsByUser(t *testing.T) {
|
|||
user := coderdtest.CreateFirstUser(t, client)
|
||||
org, err := client.Organization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
_, err = client.CreateOrganization(context.Background(), "", coderd.CreateOrganizationRequest{
|
||||
_, err = client.CreateOrganization(context.Background(), "", codersdk.CreateOrganizationRequest{
|
||||
Name: org.Name,
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
|
@ -277,7 +276,7 @@ func TestPostOrganizationsByUser(t *testing.T) {
|
|||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateOrganization(context.Background(), "", coderd.CreateOrganizationRequest{
|
||||
_, err := client.CreateOrganization(context.Background(), "", codersdk.CreateOrganizationRequest{
|
||||
Name: "new",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
@ -315,7 +314,7 @@ func TestPostWorkspacesByUser(t *testing.T) {
|
|||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
|
||||
_, err := client.CreateWorkspace(context.Background(), "", codersdk.CreateWorkspaceRequest{
|
||||
ProjectID: uuid.New(),
|
||||
Name: "workspace",
|
||||
})
|
||||
|
@ -331,14 +330,14 @@ func TestPostWorkspacesByUser(t *testing.T) {
|
|||
first := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
||||
org, err := other.CreateOrganization(context.Background(), "", coderd.CreateOrganizationRequest{
|
||||
org, err := other.CreateOrganization(context.Background(), "", codersdk.CreateOrganizationRequest{
|
||||
Name: "another",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
version := coderdtest.CreateProjectVersion(t, other, org.ID, nil)
|
||||
project := coderdtest.CreateProject(t, other, org.ID, version.ID)
|
||||
|
||||
_, err = client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
|
||||
_, err = client.CreateWorkspace(context.Background(), "", codersdk.CreateWorkspaceRequest{
|
||||
ProjectID: project.ID,
|
||||
Name: "workspace",
|
||||
})
|
||||
|
@ -351,11 +350,13 @@ func TestPostWorkspacesByUser(t *testing.T) {
|
|||
t.Run("AlreadyExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
|
||||
_, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
|
||||
_, err := client.CreateWorkspace(context.Background(), "", codersdk.CreateWorkspaceRequest{
|
||||
ProjectID: project.ID,
|
||||
Name: workspace.Name,
|
||||
})
|
||||
|
@ -368,9 +369,11 @@ func TestPostWorkspacesByUser(t *testing.T) {
|
|||
t.Run("Create", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
_ = coderdtest.CreateWorkspace(t, client, "", project.ID)
|
||||
})
|
||||
}
|
||||
|
@ -387,9 +390,11 @@ func TestWorkspacesByUser(t *testing.T) {
|
|||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
_ = coderdtest.CreateWorkspace(t, client, "", project.ID)
|
||||
workspaces, err := client.WorkspacesByUser(context.Background(), "")
|
||||
require.NoError(t, err)
|
||||
|
@ -411,9 +416,11 @@ func TestWorkspaceByUserAndName(t *testing.T) {
|
|||
t.Run("Get", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
|
||||
_, err := client.WorkspaceByName(context.Background(), "", workspace.Name)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -1,34 +1,18 @@
|
|||
package coderd
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
"github.com/coder/coder/httpmw"
|
||||
)
|
||||
|
||||
// WorkspaceBuild is an at-point representation of a workspace state.
|
||||
// Iterate on before/after to determine a chronological history.
|
||||
type WorkspaceBuild struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
WorkspaceID uuid.UUID `json:"workspace_id"`
|
||||
ProjectVersionID uuid.UUID `json:"project_version_id"`
|
||||
BeforeID uuid.UUID `json:"before_id"`
|
||||
AfterID uuid.UUID `json:"after_id"`
|
||||
Name string `json:"name"`
|
||||
Transition database.WorkspaceTransition `json:"transition"`
|
||||
Initiator string `json:"initiator"`
|
||||
Job ProvisionerJob `json:"job"`
|
||||
}
|
||||
|
||||
func (api *api) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
|
||||
workspaceBuild := httpmw.WorkspaceBuildParam(r)
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
|
||||
|
@ -42,6 +26,45 @@ func (api *api) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
|
|||
render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job)))
|
||||
}
|
||||
|
||||
func (api *api) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) {
|
||||
workspaceBuild := httpmw.WorkspaceBuildParam(r)
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if job.CompletedAt.Valid {
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: "Job has already completed!",
|
||||
})
|
||||
return
|
||||
}
|
||||
if job.CanceledAt.Valid {
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: "Job has already been marked as canceled!",
|
||||
})
|
||||
return
|
||||
}
|
||||
err = api.Database.UpdateProvisionerJobWithCancelByID(r.Context(), database.UpdateProvisionerJobWithCancelByIDParams{
|
||||
ID: job.ID,
|
||||
CanceledAt: sql.NullTime{
|
||||
Time: database.Now(),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("update provisioner job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(rw, http.StatusOK, httpapi.Response{
|
||||
Message: "Job has been marked as canceled...",
|
||||
})
|
||||
}
|
||||
|
||||
func (api *api) workspaceBuildResources(rw http.ResponseWriter, r *http.Request) {
|
||||
workspaceBuild := httpmw.WorkspaceBuildParam(r)
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
|
||||
|
@ -66,9 +89,9 @@ func (api *api) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) {
|
|||
api.provisionerJobLogs(rw, r, job)
|
||||
}
|
||||
|
||||
func convertWorkspaceBuild(workspaceBuild database.WorkspaceBuild, job ProvisionerJob) WorkspaceBuild {
|
||||
func convertWorkspaceBuild(workspaceBuild database.WorkspaceBuild, job codersdk.ProvisionerJob) codersdk.WorkspaceBuild {
|
||||
//nolint:unconvert
|
||||
return WorkspaceBuild(WorkspaceBuild{
|
||||
return codersdk.WorkspaceBuild{
|
||||
ID: workspaceBuild.ID,
|
||||
CreatedAt: workspaceBuild.CreatedAt,
|
||||
UpdatedAt: workspaceBuild.UpdatedAt,
|
||||
|
@ -80,15 +103,16 @@ func convertWorkspaceBuild(workspaceBuild database.WorkspaceBuild, job Provision
|
|||
Transition: workspaceBuild.Transition,
|
||||
Initiator: workspaceBuild.Initiator,
|
||||
Job: job,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func convertWorkspaceResource(resource database.WorkspaceResource, agent *WorkspaceAgent) WorkspaceResource {
|
||||
return WorkspaceResource{
|
||||
func convertWorkspaceResource(resource database.WorkspaceResource, agent *codersdk.WorkspaceAgent) codersdk.WorkspaceResource {
|
||||
return codersdk.WorkspaceResource{
|
||||
ID: resource.ID,
|
||||
CreatedAt: resource.CreatedAt,
|
||||
JobID: resource.JobID,
|
||||
Transition: resource.Transition,
|
||||
Address: resource.Address,
|
||||
Type: resource.Type,
|
||||
Name: resource.Name,
|
||||
Agent: agent,
|
||||
|
|
|
@ -8,10 +8,8 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/provisioner/echo"
|
||||
"github.com/coder/coder/provisionersdk/proto"
|
||||
)
|
||||
|
@ -25,13 +23,44 @@ func TestWorkspaceBuild(t *testing.T) {
|
|||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
_, err := client.WorkspaceBuild(context.Background(), workspace.LatestBuild.ID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestPatchCancelWorkspaceBuild(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
Provision: []*proto.Provision_Response{{
|
||||
Type: &proto.Provision_Response_Log{
|
||||
Log: &proto.Log{},
|
||||
},
|
||||
}},
|
||||
ProvisionDryRun: echo.ProvisionComplete,
|
||||
})
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
|
||||
var build codersdk.WorkspaceBuild
|
||||
require.Eventually(t, func() bool {
|
||||
var err error
|
||||
build, err = client.WorkspaceBuild(context.Background(), workspace.LatestBuild.ID)
|
||||
require.NoError(t, err)
|
||||
return build.Job.Status == codersdk.ProvisionerJobRunning
|
||||
}, 5*time.Second, 25*time.Millisecond)
|
||||
err := client.CancelWorkspaceBuild(context.Background(), build.ID)
|
||||
require.NoError(t, err)
|
||||
_, err = client.WorkspaceBuild(context.Background(), build.ID)
|
||||
require.NoError(t, err)
|
||||
require.Eventually(t, func() bool {
|
||||
var err error
|
||||
build, err = client.WorkspaceBuild(context.Background(), build.ID)
|
||||
require.NoError(t, err)
|
||||
// The echo provisioner doesn't respond to a shutdown request,
|
||||
// so the job cancel will time out and fail.
|
||||
return build.Job.Status == codersdk.ProvisionerJobFailed
|
||||
}, 5*time.Second, 25*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestWorkspaceBuildResources(t *testing.T) {
|
||||
|
@ -46,12 +75,7 @@ func TestWorkspaceBuildResources(t *testing.T) {
|
|||
closeDaemon.Close()
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = client.WorkspaceResourcesByBuild(context.Background(), build.ID)
|
||||
_, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
|
||||
|
@ -84,13 +108,8 @@ func TestWorkspaceBuildResources(t *testing.T) {
|
|||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
|
||||
resources, err := client.WorkspaceResourcesByBuild(context.Background(), build.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
resources, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resources)
|
||||
require.Len(t, resources, 2)
|
||||
|
@ -136,14 +155,9 @@ func TestWorkspaceBuildLogs(t *testing.T) {
|
|||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancelFunc)
|
||||
logs, err := client.WorkspaceBuildLogsAfter(ctx, build.ID, before)
|
||||
logs, err := client.WorkspaceBuildLogsAfter(ctx, workspace.LatestBuild.ID, before)
|
||||
require.NoError(t, err)
|
||||
log := <-logs
|
||||
require.Equal(t, "example", log.Output)
|
||||
|
|
|
@ -9,27 +9,18 @@ import (
|
|||
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
type GoogleInstanceIdentityToken struct {
|
||||
JSONWebToken string `json:"json_web_token" validate:"required"`
|
||||
}
|
||||
|
||||
// WorkspaceAgentAuthenticateResponse is returned when an instance ID
|
||||
// has been exchanged for a session token.
|
||||
type WorkspaceAgentAuthenticateResponse struct {
|
||||
SessionToken string `json:"session_token"`
|
||||
}
|
||||
|
||||
// Google Compute Engine supports instance identity verification:
|
||||
// https://cloud.google.com/compute/docs/instances/verifying-instance-identity
|
||||
// Using this, we can exchange a signed instance payload for an agent token.
|
||||
func (api *api) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
|
||||
var req GoogleInstanceIdentityToken
|
||||
var req codersdk.GoogleInstanceIdentityToken
|
||||
if !httpapi.Read(rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
@ -121,7 +112,7 @@ func (api *api) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter,
|
|||
return
|
||||
}
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(rw, r, WorkspaceAgentAuthenticateResponse{
|
||||
render.JSON(rw, r, codersdk.WorkspaceAgentAuthenticateResponse{
|
||||
SessionToken: agent.AuthToken.String(),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -19,11 +19,9 @@ import (
|
|||
"google.golang.org/api/idtoken"
|
||||
"google.golang.org/api/option"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/cryptorand"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/provisioner/echo"
|
||||
"github.com/coder/coder/provisionersdk/proto"
|
||||
)
|
||||
|
@ -69,8 +67,22 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) {
|
|||
user := coderdtest.CreateFirstUser(t, client)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionDryRun: echo.ProvisionComplete,
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionDryRun: []*proto.Provision_Response{{
|
||||
Type: &proto.Provision_Response_Complete{
|
||||
Complete: &proto.Provision_Complete{
|
||||
Resources: []*proto.Resource{{
|
||||
Name: "somename",
|
||||
Type: "someinstance",
|
||||
Agent: &proto.Agent{
|
||||
Auth: &proto.Agent_GoogleInstanceIdentity{
|
||||
GoogleInstanceIdentity: &proto.GoogleInstanceIdentityAuth{},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
Provision: []*proto.Provision_Response{{
|
||||
Type: &proto.Provision_Response_Complete{
|
||||
Complete: &proto.Provision_Complete{
|
||||
|
@ -92,14 +104,9 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) {
|
|||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
_, err = client.AuthWorkspaceGoogleInstanceIdentity(context.Background(), "", createMetadataClient(signedKey))
|
||||
_, err := client.AuthWorkspaceGoogleInstanceIdentity(context.Background(), "", createMetadataClient(signedKey))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -9,11 +9,13 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/yamux"
|
||||
"golang.org/x/xerrors"
|
||||
"nhooyr.io/websocket"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
"github.com/coder/coder/httpmw"
|
||||
|
@ -22,46 +24,6 @@ import (
|
|||
"github.com/coder/coder/provisionersdk"
|
||||
)
|
||||
|
||||
type WorkspaceResource struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
JobID uuid.UUID `json:"job_id"`
|
||||
Transition database.WorkspaceTransition `json:"workspace_transition"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Agent *WorkspaceAgent `json:"agent,omitempty"`
|
||||
}
|
||||
|
||||
type WorkspaceAgent struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ResourceID uuid.UUID `json:"resource_id"`
|
||||
InstanceID string `json:"instance_id,omitempty"`
|
||||
EnvironmentVariables map[string]string `json:"environment_variables"`
|
||||
StartupScript string `json:"startup_script,omitempty"`
|
||||
}
|
||||
|
||||
type WorkspaceAgentResourceMetadata struct {
|
||||
MemoryTotal uint64 `json:"memory_total"`
|
||||
DiskTotal uint64 `json:"disk_total"`
|
||||
CPUCores uint64 `json:"cpu_cores"`
|
||||
CPUModel string `json:"cpu_model"`
|
||||
CPUMhz float64 `json:"cpu_mhz"`
|
||||
}
|
||||
|
||||
type WorkspaceAgentInstanceMetadata struct {
|
||||
JailOrchestrator string `json:"jail_orchestrator"`
|
||||
OperatingSystem string `json:"operating_system"`
|
||||
Platform string `json:"platform"`
|
||||
PlatformFamily string `json:"platform_family"`
|
||||
KernelVersion string `json:"kernel_version"`
|
||||
KernelArchitecture string `json:"kernel_architecture"`
|
||||
Cloud string `json:"cloud"`
|
||||
Jail string `json:"jail"`
|
||||
VNC bool `json:"vnc"`
|
||||
}
|
||||
|
||||
func (api *api) workspaceResource(rw http.ResponseWriter, r *http.Request) {
|
||||
workspaceBuild := httpmw.WorkspaceBuildParam(r)
|
||||
workspaceResource := httpmw.WorkspaceResourceParam(r)
|
||||
|
@ -78,7 +40,7 @@ func (api *api) workspaceResource(rw http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
return
|
||||
}
|
||||
var apiAgent *WorkspaceAgent
|
||||
var apiAgent *codersdk.WorkspaceAgent
|
||||
if workspaceResource.AgentID.Valid {
|
||||
agent, err := api.Database.GetWorkspaceAgentByResourceID(r.Context(), workspaceResource.ID)
|
||||
if err != nil {
|
||||
|
@ -87,7 +49,7 @@ func (api *api) workspaceResource(rw http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
return
|
||||
}
|
||||
convertedAgent, err := convertWorkspaceAgent(agent)
|
||||
convertedAgent, err := convertWorkspaceAgent(agent, api.AgentConnectionUpdateFrequency)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("convert provisioner job agent: %s", err),
|
||||
|
@ -163,6 +125,16 @@ func (api *api) workspaceAgentListen(rw http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
return
|
||||
}
|
||||
resource, err := api.Database.GetWorkspaceResourceByID(r.Context(), agent.ResourceID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: fmt.Sprintf("accept websocket: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
api.Logger.Info(r.Context(), "accepting agent", slog.F("resource", resource), slog.F("agent", agent))
|
||||
|
||||
defer func() {
|
||||
_ = conn.Close(websocket.StatusNormalClosure, "")
|
||||
}()
|
||||
|
@ -183,31 +155,57 @@ func (api *api) workspaceAgentListen(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
defer closer.Close()
|
||||
err = api.Database.UpdateWorkspaceAgentByID(r.Context(), database.UpdateWorkspaceAgentByIDParams{
|
||||
ID: agent.ID,
|
||||
UpdatedAt: sql.NullTime{
|
||||
firstConnectedAt := agent.FirstConnectedAt
|
||||
if !firstConnectedAt.Valid {
|
||||
firstConnectedAt = sql.NullTime{
|
||||
Time: database.Now(),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
lastConnectedAt := sql.NullTime{
|
||||
Time: database.Now(),
|
||||
Valid: true,
|
||||
}
|
||||
disconnectedAt := agent.DisconnectedAt
|
||||
updateConnectionTimes := func() error {
|
||||
err = api.Database.UpdateWorkspaceAgentConnectionByID(r.Context(), database.UpdateWorkspaceAgentConnectionByIDParams{
|
||||
ID: agent.ID,
|
||||
FirstConnectedAt: firstConnectedAt,
|
||||
LastConnectedAt: lastConnectedAt,
|
||||
DisconnectedAt: disconnectedAt,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
disconnectedAt = sql.NullTime{
|
||||
Time: database.Now(),
|
||||
Valid: true,
|
||||
}
|
||||
_ = updateConnectionTimes()
|
||||
}()
|
||||
|
||||
err = updateConnectionTimes()
|
||||
if err != nil {
|
||||
_ = conn.Close(websocket.StatusAbnormalClosure, err.Error())
|
||||
return
|
||||
}
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
|
||||
ticker := time.NewTicker(api.AgentConnectionUpdateFrequency)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-session.CloseChan():
|
||||
return
|
||||
case <-ticker.C:
|
||||
err = api.Database.UpdateWorkspaceAgentByID(r.Context(), database.UpdateWorkspaceAgentByIDParams{
|
||||
ID: agent.ID,
|
||||
UpdatedAt: sql.NullTime{
|
||||
Time: database.Now(),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
lastConnectedAt = sql.NullTime{
|
||||
Time: database.Now(),
|
||||
Valid: true,
|
||||
}
|
||||
err = updateConnectionTimes()
|
||||
if err != nil {
|
||||
_ = conn.Close(websocket.StatusAbnormalClosure, err.Error())
|
||||
return
|
||||
|
@ -216,21 +214,49 @@ func (api *api) workspaceAgentListen(rw http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func convertWorkspaceAgent(agent database.WorkspaceAgent) (WorkspaceAgent, error) {
|
||||
func convertWorkspaceAgent(dbAgent database.WorkspaceAgent, agentUpdateFrequency time.Duration) (codersdk.WorkspaceAgent, error) {
|
||||
var envs map[string]string
|
||||
if agent.EnvironmentVariables.Valid {
|
||||
err := json.Unmarshal(agent.EnvironmentVariables.RawMessage, &envs)
|
||||
if dbAgent.EnvironmentVariables.Valid {
|
||||
err := json.Unmarshal(dbAgent.EnvironmentVariables.RawMessage, &envs)
|
||||
if err != nil {
|
||||
return WorkspaceAgent{}, xerrors.Errorf("unmarshal: %w", err)
|
||||
return codersdk.WorkspaceAgent{}, xerrors.Errorf("unmarshal: %w", err)
|
||||
}
|
||||
}
|
||||
return WorkspaceAgent{
|
||||
ID: agent.ID,
|
||||
CreatedAt: agent.CreatedAt,
|
||||
UpdatedAt: agent.UpdatedAt.Time,
|
||||
ResourceID: agent.ResourceID,
|
||||
InstanceID: agent.AuthInstanceID.String,
|
||||
StartupScript: agent.StartupScript.String,
|
||||
agent := codersdk.WorkspaceAgent{
|
||||
ID: dbAgent.ID,
|
||||
CreatedAt: dbAgent.CreatedAt,
|
||||
UpdatedAt: dbAgent.UpdatedAt,
|
||||
ResourceID: dbAgent.ResourceID,
|
||||
InstanceID: dbAgent.AuthInstanceID.String,
|
||||
StartupScript: dbAgent.StartupScript.String,
|
||||
EnvironmentVariables: envs,
|
||||
}, nil
|
||||
}
|
||||
if dbAgent.FirstConnectedAt.Valid {
|
||||
agent.FirstConnectedAt = &dbAgent.FirstConnectedAt.Time
|
||||
}
|
||||
if dbAgent.LastConnectedAt.Valid {
|
||||
agent.LastConnectedAt = &dbAgent.LastConnectedAt.Time
|
||||
}
|
||||
if dbAgent.DisconnectedAt.Valid {
|
||||
agent.DisconnectedAt = &dbAgent.DisconnectedAt.Time
|
||||
}
|
||||
switch {
|
||||
case !dbAgent.FirstConnectedAt.Valid:
|
||||
// If the agent never connected, it's waiting for the compute
|
||||
// to start up.
|
||||
agent.Status = codersdk.WorkspaceAgentWaiting
|
||||
case dbAgent.DisconnectedAt.Time.After(dbAgent.LastConnectedAt.Time):
|
||||
// If we've disconnected after our last connection, we know the
|
||||
// agent is no longer connected.
|
||||
agent.Status = codersdk.WorkspaceAgentDisconnected
|
||||
case agentUpdateFrequency*2 >= database.Now().Sub(dbAgent.LastConnectedAt.Time):
|
||||
// The connection updated it's timestamp within the update frequency.
|
||||
// We multiply by two to allow for some lag.
|
||||
agent.Status = codersdk.WorkspaceAgentConnected
|
||||
case database.Now().Sub(dbAgent.LastConnectedAt.Time) > agentUpdateFrequency*2:
|
||||
// The connection died without updating the last connected.
|
||||
agent.Status = codersdk.WorkspaceAgentDisconnected
|
||||
}
|
||||
|
||||
return agent, nil
|
||||
}
|
||||
|
|
|
@ -11,10 +11,8 @@ import (
|
|||
"cdr.dev/slog/sloggers/slogtest"
|
||||
|
||||
"github.com/coder/coder/agent"
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/peer"
|
||||
"github.com/coder/coder/peerbroker"
|
||||
"github.com/coder/coder/provisioner/echo"
|
||||
|
@ -48,13 +46,8 @@ func TestWorkspaceResource(t *testing.T) {
|
|||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
|
||||
resources, err := client.WorkspaceResourcesByBuild(context.Background(), build.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
resources, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID)
|
||||
require.NoError(t, err)
|
||||
_, err = client.WorkspaceResource(context.Background(), resources[0].ID)
|
||||
require.NoError(t, err)
|
||||
|
@ -90,12 +83,7 @@ func TestWorkspaceAgentListen(t *testing.T) {
|
|||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
daemonCloser.Close()
|
||||
|
||||
agentClient := codersdk.New(client.URL)
|
||||
|
@ -106,7 +94,7 @@ func TestWorkspaceAgentListen(t *testing.T) {
|
|||
t.Cleanup(func() {
|
||||
_ = agentCloser.Close()
|
||||
})
|
||||
resources := coderdtest.AwaitWorkspaceAgents(t, client, build.ID)
|
||||
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
|
||||
workspaceClient, err := client.DialWorkspaceAgent(context.Background(), resources[0].ID)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
|
|
|
@ -13,32 +13,51 @@ import (
|
|||
"github.com/moby/moby/pkg/namesgenerator"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
"github.com/coder/coder/httpmw"
|
||||
)
|
||||
|
||||
// Workspace is a per-user deployment of a project. It tracks
|
||||
// project versions, and can be updated.
|
||||
type Workspace database.Workspace
|
||||
|
||||
// CreateWorkspaceBuildRequest provides options to update the latest workspace build.
|
||||
type CreateWorkspaceBuildRequest struct {
|
||||
ProjectVersionID uuid.UUID `json:"project_version_id" validate:"required"`
|
||||
Transition database.WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"`
|
||||
}
|
||||
|
||||
func (*api) workspace(rw http.ResponseWriter, r *http.Request) {
|
||||
func (api *api) workspace(rw http.ResponseWriter, r *http.Request) {
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(rw, r, convertWorkspace(workspace))
|
||||
|
||||
build, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace build: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), build.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace build job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
project, err := api.Database.GetProjectByID(r.Context(), workspace.ProjectID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get project: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
render.JSON(rw, r, convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), project))
|
||||
}
|
||||
|
||||
func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
|
||||
builds, err := api.Database.GetWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace builds: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
jobIDs := make([]uuid.UUID, 0, len(builds))
|
||||
|
@ -46,6 +65,9 @@ func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||
jobIDs = append(jobIDs, version.JobID)
|
||||
}
|
||||
jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get jobs: %s", err),
|
||||
|
@ -57,7 +79,7 @@ func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||
jobByID[job.ID.String()] = job
|
||||
}
|
||||
|
||||
apiBuilds := make([]WorkspaceBuild, 0)
|
||||
apiBuilds := make([]codersdk.WorkspaceBuild, 0)
|
||||
for _, build := range builds {
|
||||
job, exists := jobByID[build.JobID.String()]
|
||||
if !exists {
|
||||
|
@ -76,10 +98,20 @@ func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||
func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
||||
apiKey := httpmw.APIKey(r)
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
var createBuild CreateWorkspaceBuildRequest
|
||||
var createBuild codersdk.CreateWorkspaceBuildRequest
|
||||
if !httpapi.Read(rw, r, &createBuild) {
|
||||
return
|
||||
}
|
||||
if createBuild.ProjectVersionID == uuid.Nil {
|
||||
latestBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get latest workspace build: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
createBuild.ProjectVersionID = latestBuild.ProjectVersionID
|
||||
}
|
||||
projectVersion, err := api.Database.GetProjectVersionByID(r.Context(), createBuild.ProjectVersionID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
|
@ -106,19 +138,19 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
projectVersionJobStatus := convertProvisionerJob(projectVersionJob).Status
|
||||
switch projectVersionJobStatus {
|
||||
case ProvisionerJobPending, ProvisionerJobRunning:
|
||||
case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning:
|
||||
httpapi.Write(rw, http.StatusNotAcceptable, httpapi.Response{
|
||||
Message: fmt.Sprintf("The provided project version is %s. Wait for it to complete importing!", projectVersionJobStatus),
|
||||
})
|
||||
return
|
||||
case ProvisionerJobFailed:
|
||||
case codersdk.ProvisionerJobFailed:
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: fmt.Sprintf("The provided project version %q has failed to import. You cannot create workspaces using it!", projectVersion.Name),
|
||||
Message: fmt.Sprintf("The provided project version %q has failed to import: %q. You cannot build workspaces with it!", projectVersion.Name, projectVersionJob.Error.String),
|
||||
})
|
||||
return
|
||||
case ProvisionerJobCancelled:
|
||||
case codersdk.ProvisionerJobCanceled:
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: "The provided project version was canceled during import. You cannot create workspaces using it!",
|
||||
Message: "The provided project version was canceled during import. You cannot builds workspaces with it!",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -189,6 +221,7 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||
ProjectVersionID: projectVersion.ID,
|
||||
BeforeID: priorHistoryID,
|
||||
Name: namesgenerator.GetRandomName(1),
|
||||
ProvisionerState: priorHistory.ProvisionerState,
|
||||
Initiator: apiKey.UserID,
|
||||
Transition: createBuild.Transition,
|
||||
JobID: provisionerJob.ID,
|
||||
|
@ -226,33 +259,6 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||
render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(provisionerJob)))
|
||||
}
|
||||
|
||||
func (api *api) workspaceBuildLatest(rw http.ResponseWriter, r *http.Request) {
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: "no workspace build found",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace build by name: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job)))
|
||||
}
|
||||
|
||||
func (api *api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) {
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
workspaceBuildName := chi.URLParam(r, "workspacebuildname")
|
||||
|
@ -284,6 +290,16 @@ func (api *api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) {
|
|||
render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job)))
|
||||
}
|
||||
|
||||
func convertWorkspace(workspace database.Workspace) Workspace {
|
||||
return Workspace(workspace)
|
||||
func convertWorkspace(workspace database.Workspace, workspaceBuild codersdk.WorkspaceBuild, project database.Project) codersdk.Workspace {
|
||||
return codersdk.Workspace{
|
||||
ID: workspace.ID,
|
||||
CreatedAt: workspace.CreatedAt,
|
||||
UpdatedAt: workspace.UpdatedAt,
|
||||
OwnerID: workspace.OwnerID,
|
||||
ProjectID: workspace.ProjectID,
|
||||
LatestBuild: workspaceBuild,
|
||||
ProjectName: project.Name,
|
||||
Outdated: workspaceBuild.ProjectVersionID.String() != project.ActiveVersionID.String(),
|
||||
Name: workspace.Name,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
|
@ -20,23 +19,43 @@ func TestWorkspace(t *testing.T) {
|
|||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
|
||||
_, err := client.Workspace(context.Background(), workspace.ID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWorkspaceBuilds(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Single", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
_, err := client.WorkspaceBuilds(context.Background(), workspace.ID)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostWorkspaceBuild(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NoProjectVersion", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
_, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
_, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: uuid.New(),
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
|
@ -56,12 +75,10 @@ func TestPostWorkspaceBuild(t *testing.T) {
|
|||
})
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
_, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
_, err := client.CreateWorkspace(context.Background(), "", codersdk.CreateWorkspaceRequest{
|
||||
ProjectID: project.ID,
|
||||
Name: "workspace",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
|
||||
|
@ -78,12 +95,7 @@ func TestPostWorkspaceBuild(t *testing.T) {
|
|||
// Close here so workspace build doesn't process!
|
||||
closeDaemon.Close()
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
_, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
_, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
|
@ -102,28 +114,20 @@ func TestPostWorkspaceBuild(t *testing.T) {
|
|||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
firstBuild, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, firstBuild.ID)
|
||||
secondBuild, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, firstBuild.ID.String(), secondBuild.BeforeID.String())
|
||||
require.Equal(t, workspace.LatestBuild.ID.String(), build.BeforeID.String())
|
||||
|
||||
firstBuild, err = client.WorkspaceBuild(context.Background(), firstBuild.ID)
|
||||
firstBuild, err := client.WorkspaceBuild(context.Background(), workspace.LatestBuild.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, secondBuild.ID.String(), firstBuild.AfterID.String())
|
||||
require.Equal(t, build.ID.String(), firstBuild.AfterID.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspaceBuildLatest(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("None", func(t *testing.T) {
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
@ -132,28 +136,17 @@ func TestWorkspaceBuildLatest(t *testing.T) {
|
|||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
_, err := client.WorkspaceBuildLatest(context.Background(), workspace.ID)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
_, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
Transition: database.WorkspaceTransitionDelete,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = client.WorkspaceBuildLatest(context.Background(), workspace.ID)
|
||||
require.Equal(t, workspace.LatestBuild.ID.String(), build.BeforeID.String())
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
|
||||
|
||||
workspaces, err := client.WorkspacesByUser(context.Background(), user.UserID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 0)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -183,10 +176,7 @@ func TestWorkspaceBuildByName(t *testing.T) {
|
|||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
build, err := client.WorkspaceBuild(context.Background(), workspace.LatestBuild.ID)
|
||||
require.NoError(t, err)
|
||||
_, err = client.WorkspaceBuildByName(context.Background(), workspace.ID, build.Name)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -6,28 +6,31 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
)
|
||||
|
||||
const (
|
||||
ContentTypeTar = "application/x-tar"
|
||||
)
|
||||
|
||||
// UploadResponse contains the hash to reference the uploaded file.
|
||||
type UploadResponse struct {
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
|
||||
// Upload uploads an arbitrary file with the content type provided.
|
||||
// This is used to upload a source-code archive.
|
||||
func (c *Client) Upload(ctx context.Context, contentType string, content []byte) (coderd.UploadResponse, error) {
|
||||
func (c *Client) Upload(ctx context.Context, contentType string, content []byte) (UploadResponse, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, "/api/v2/files", content, func(r *http.Request) {
|
||||
r.Header.Set("Content-Type", contentType)
|
||||
})
|
||||
if err != nil {
|
||||
return coderd.UploadResponse{}, err
|
||||
return UploadResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated && res.StatusCode != http.StatusOK {
|
||||
return coderd.UploadResponse{}, readBodyAsError(res)
|
||||
return UploadResponse{}, readBodyAsError(res)
|
||||
}
|
||||
var resp coderd.UploadResponse
|
||||
var resp UploadResponse
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,25 +5,63 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
func (c *Client) Organization(ctx context.Context, id string) (coderd.Organization, error) {
|
||||
// Organization is the JSON representation of a Coder organization.
|
||||
type Organization struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required"`
|
||||
UpdatedAt time.Time `json:"updated_at" validate:"required"`
|
||||
}
|
||||
|
||||
// CreateProjectVersionRequest enables callers to create a new Project Version.
|
||||
type CreateProjectVersionRequest struct {
|
||||
// ProjectID optionally associates a version with a project.
|
||||
ProjectID uuid.UUID `json:"project_id"`
|
||||
|
||||
StorageMethod database.ProvisionerStorageMethod `json:"storage_method" validate:"oneof=file,required"`
|
||||
StorageSource string `json:"storage_source" validate:"required"`
|
||||
Provisioner database.ProvisionerType `json:"provisioner" validate:"oneof=terraform echo,required"`
|
||||
// ParameterValues allows for additional parameters to be provided
|
||||
// during the dry-run provision stage.
|
||||
ParameterValues []CreateParameterRequest `json:"parameter_values"`
|
||||
}
|
||||
|
||||
// CreateProjectRequest provides options when creating a project.
|
||||
type CreateProjectRequest struct {
|
||||
Name string `json:"name" validate:"username,required"`
|
||||
|
||||
// VersionID is an in-progress or completed job to use as
|
||||
// an initial version of the project.
|
||||
//
|
||||
// This is required on creation to enable a user-flow of validating a
|
||||
// project works. There is no reason the data-model cannot support
|
||||
// empty projects, but it doesn't make sense for users.
|
||||
VersionID uuid.UUID `json:"project_version_id" validate:"required"`
|
||||
ParameterValues []CreateParameterRequest `json:"parameter_values"`
|
||||
}
|
||||
|
||||
func (c *Client) Organization(ctx context.Context, id string) (Organization, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s", id), nil)
|
||||
if err != nil {
|
||||
return coderd.Organization{}, err
|
||||
return Organization{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.Organization{}, readBodyAsError(res)
|
||||
return Organization{}, readBodyAsError(res)
|
||||
}
|
||||
var organization coderd.Organization
|
||||
var organization Organization
|
||||
return organization, json.NewDecoder(res.Body).Decode(&organization)
|
||||
}
|
||||
|
||||
// ProvisionerDaemonsByOrganization returns provisioner daemons available for an organization.
|
||||
func (c *Client) ProvisionerDaemonsByOrganization(ctx context.Context, organization string) ([]coderd.ProvisionerDaemon, error) {
|
||||
func (c *Client) ProvisionerDaemonsByOrganization(ctx context.Context, organization string) ([]ProvisionerDaemon, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons", organization), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -32,41 +70,41 @@ func (c *Client) ProvisionerDaemonsByOrganization(ctx context.Context, organizat
|
|||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var daemons []coderd.ProvisionerDaemon
|
||||
var daemons []ProvisionerDaemon
|
||||
return daemons, json.NewDecoder(res.Body).Decode(&daemons)
|
||||
}
|
||||
|
||||
// CreateProjectVersion processes source-code and optionally associates the version with a project.
|
||||
// Executing without a project is useful for validating source-code.
|
||||
func (c *Client) CreateProjectVersion(ctx context.Context, organization string, req coderd.CreateProjectVersionRequest) (coderd.ProjectVersion, error) {
|
||||
func (c *Client) CreateProjectVersion(ctx context.Context, organization string, req CreateProjectVersionRequest) (ProjectVersion, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/projectversions", organization), req)
|
||||
if err != nil {
|
||||
return coderd.ProjectVersion{}, err
|
||||
return ProjectVersion{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.ProjectVersion{}, readBodyAsError(res)
|
||||
return ProjectVersion{}, readBodyAsError(res)
|
||||
}
|
||||
var projectVersion coderd.ProjectVersion
|
||||
var projectVersion ProjectVersion
|
||||
return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion)
|
||||
}
|
||||
|
||||
// CreateProject creates a new project inside an organization.
|
||||
func (c *Client) CreateProject(ctx context.Context, organization string, request coderd.CreateProjectRequest) (coderd.Project, error) {
|
||||
func (c *Client) CreateProject(ctx context.Context, organization string, request CreateProjectRequest) (Project, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/projects", organization), request)
|
||||
if err != nil {
|
||||
return coderd.Project{}, err
|
||||
return Project{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.Project{}, readBodyAsError(res)
|
||||
return Project{}, readBodyAsError(res)
|
||||
}
|
||||
var project coderd.Project
|
||||
var project Project
|
||||
return project, json.NewDecoder(res.Body).Decode(&project)
|
||||
}
|
||||
|
||||
// ProjectsByOrganization lists all projects inside of an organization.
|
||||
func (c *Client) ProjectsByOrganization(ctx context.Context, organization string) ([]coderd.Project, error) {
|
||||
func (c *Client) ProjectsByOrganization(ctx context.Context, organization string) ([]Project, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/projects", organization), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -75,20 +113,20 @@ func (c *Client) ProjectsByOrganization(ctx context.Context, organization string
|
|||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var projects []coderd.Project
|
||||
var projects []Project
|
||||
return projects, json.NewDecoder(res.Body).Decode(&projects)
|
||||
}
|
||||
|
||||
// ProjectByName finds a project inside the organization provided with a case-insensitive name.
|
||||
func (c *Client) ProjectByName(ctx context.Context, organization, name string) (coderd.Project, error) {
|
||||
func (c *Client) ProjectByName(ctx context.Context, organization, name string) (Project, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/projects/%s", organization, name), nil)
|
||||
if err != nil {
|
||||
return coderd.Project{}, err
|
||||
return Project{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.Project{}, readBodyAsError(res)
|
||||
return Project{}, readBodyAsError(res)
|
||||
}
|
||||
var project coderd.Project
|
||||
var project Project
|
||||
return project, json.NewDecoder(res.Body).Decode(&project)
|
||||
}
|
||||
|
|
|
@ -5,24 +5,56 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
func (c *Client) CreateParameter(ctx context.Context, scope coderd.ParameterScope, id string, req coderd.CreateParameterRequest) (coderd.Parameter, error) {
|
||||
type ParameterScope string
|
||||
|
||||
const (
|
||||
ParameterOrganization ParameterScope = "organization"
|
||||
ParameterProject ParameterScope = "project"
|
||||
ParameterUser ParameterScope = "user"
|
||||
ParameterWorkspace ParameterScope = "workspace"
|
||||
)
|
||||
|
||||
// Parameter represents a set value for the scope.
|
||||
type Parameter struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
Scope ParameterScope `db:"scope" json:"scope"`
|
||||
ScopeID string `db:"scope_id" json:"scope_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
SourceScheme database.ParameterSourceScheme `db:"source_scheme" json:"source_scheme"`
|
||||
DestinationScheme database.ParameterDestinationScheme `db:"destination_scheme" json:"destination_scheme"`
|
||||
}
|
||||
|
||||
// CreateParameterRequest is used to create a new parameter value for a scope.
|
||||
type CreateParameterRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
SourceValue string `json:"source_value" validate:"required"`
|
||||
SourceScheme database.ParameterSourceScheme `json:"source_scheme" validate:"oneof=data,required"`
|
||||
DestinationScheme database.ParameterDestinationScheme `json:"destination_scheme" validate:"oneof=environment_variable provisioner_variable,required"`
|
||||
}
|
||||
|
||||
func (c *Client) CreateParameter(ctx context.Context, scope ParameterScope, id string, req CreateParameterRequest) (Parameter, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/parameters/%s/%s", scope, id), req)
|
||||
if err != nil {
|
||||
return coderd.Parameter{}, err
|
||||
return Parameter{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.Parameter{}, readBodyAsError(res)
|
||||
return Parameter{}, readBodyAsError(res)
|
||||
}
|
||||
var param coderd.Parameter
|
||||
var param Parameter
|
||||
return param, json.NewDecoder(res.Body).Decode(¶m)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteParameter(ctx context.Context, scope coderd.ParameterScope, id, name string) error {
|
||||
func (c *Client) DeleteParameter(ctx context.Context, scope ParameterScope, id, name string) error {
|
||||
res, err := c.request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/parameters/%s/%s/%s", scope, id, name), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -34,7 +66,7 @@ func (c *Client) DeleteParameter(ctx context.Context, scope coderd.ParameterScop
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Parameters(ctx context.Context, scope coderd.ParameterScope, id string) ([]coderd.Parameter, error) {
|
||||
func (c *Client) Parameters(ctx context.Context, scope ParameterScope, id string) ([]Parameter, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/parameters/%s/%s", scope, id), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -43,6 +75,6 @@ func (c *Client) Parameters(ctx context.Context, scope coderd.ParameterScope, id
|
|||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var parameters []coderd.Parameter
|
||||
var parameters []Parameter
|
||||
return parameters, json.NewDecoder(res.Body).Decode(¶meters)
|
||||
}
|
||||
|
|
|
@ -5,28 +5,73 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
// Project is the JSON representation of a Coder project.
|
||||
// This type matches the database object for now, but is
|
||||
// abstracted for ease of change later on.
|
||||
type Project struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
OrganizationID string `json:"organization_id"`
|
||||
Name string `json:"name"`
|
||||
Provisioner database.ProvisionerType `json:"provisioner"`
|
||||
ActiveVersionID uuid.UUID `json:"active_version_id"`
|
||||
WorkspaceOwnerCount uint32 `json:"workspace_owner_count"`
|
||||
}
|
||||
|
||||
type UpdateActiveProjectVersion struct {
|
||||
ID uuid.UUID `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
// Project returns a single project.
|
||||
func (c *Client) Project(ctx context.Context, project uuid.UUID) (coderd.Project, error) {
|
||||
func (c *Client) Project(ctx context.Context, project uuid.UUID) (Project, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s", project), nil)
|
||||
if err != nil {
|
||||
return coderd.Project{}, nil
|
||||
return Project{}, nil
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.Project{}, readBodyAsError(res)
|
||||
return Project{}, readBodyAsError(res)
|
||||
}
|
||||
var resp coderd.Project
|
||||
var resp Project
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteProject(ctx context.Context, project uuid.UUID) error {
|
||||
res, err := c.request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/projects/%s", project), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return readBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateActiveProjectVersion updates the active project version to the ID provided.
|
||||
// The project version must be attached to the project.
|
||||
func (c *Client) UpdateActiveProjectVersion(ctx context.Context, project uuid.UUID, req UpdateActiveProjectVersion) error {
|
||||
res, err := c.request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/projects/%s/versions", project), req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return readBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProjectVersionsByProject lists versions associated with a project.
|
||||
func (c *Client) ProjectVersionsByProject(ctx context.Context, project uuid.UUID) ([]coderd.ProjectVersion, error) {
|
||||
func (c *Client) ProjectVersionsByProject(ctx context.Context, project uuid.UUID) ([]ProjectVersion, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/versions", project), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -35,21 +80,21 @@ func (c *Client) ProjectVersionsByProject(ctx context.Context, project uuid.UUID
|
|||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var projectVersion []coderd.ProjectVersion
|
||||
var projectVersion []ProjectVersion
|
||||
return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion)
|
||||
}
|
||||
|
||||
// ProjectVersionByName returns a project version by it's friendly name.
|
||||
// This is used for path-based routing. Like: /projects/example/versions/helloworld
|
||||
func (c *Client) ProjectVersionByName(ctx context.Context, project uuid.UUID, name string) (coderd.ProjectVersion, error) {
|
||||
func (c *Client) ProjectVersionByName(ctx context.Context, project uuid.UUID, name string) (ProjectVersion, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/versions/%s", project, name), nil)
|
||||
if err != nil {
|
||||
return coderd.ProjectVersion{}, err
|
||||
return ProjectVersion{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.ProjectVersion{}, readBodyAsError(res)
|
||||
return ProjectVersion{}, readBodyAsError(res)
|
||||
}
|
||||
var projectVersion coderd.ProjectVersion
|
||||
var projectVersion ProjectVersion
|
||||
return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion)
|
||||
}
|
||||
|
|
|
@ -9,25 +9,55 @@ import (
|
|||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/parameter"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
// ProjectVersion represents a single version of a project.
|
||||
type ProjectVersion struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ProjectID *uuid.UUID `json:"project_id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Name string `json:"name"`
|
||||
Job ProvisionerJob `json:"job"`
|
||||
}
|
||||
|
||||
// ProjectVersionParameterSchema represents a parameter parsed from project version source.
|
||||
type ProjectVersionParameterSchema database.ParameterSchema
|
||||
|
||||
// ProjectVersionParameter represents a computed parameter value.
|
||||
type ProjectVersionParameter parameter.ComputedValue
|
||||
|
||||
// ProjectVersion returns a project version by ID.
|
||||
func (c *Client) ProjectVersion(ctx context.Context, id uuid.UUID) (coderd.ProjectVersion, error) {
|
||||
func (c *Client) ProjectVersion(ctx context.Context, id uuid.UUID) (ProjectVersion, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s", id), nil)
|
||||
if err != nil {
|
||||
return coderd.ProjectVersion{}, err
|
||||
return ProjectVersion{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.ProjectVersion{}, readBodyAsError(res)
|
||||
return ProjectVersion{}, readBodyAsError(res)
|
||||
}
|
||||
var version coderd.ProjectVersion
|
||||
var version ProjectVersion
|
||||
return version, json.NewDecoder(res.Body).Decode(&version)
|
||||
}
|
||||
|
||||
// CancelProjectVersion marks a project version job as canceled.
|
||||
func (c *Client) CancelProjectVersion(ctx context.Context, version uuid.UUID) error {
|
||||
res, err := c.request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/projectversions/%s/cancel", version), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return readBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProjectVersionSchema returns schemas for a project version by ID.
|
||||
func (c *Client) ProjectVersionSchema(ctx context.Context, version uuid.UUID) ([]coderd.ProjectVersionParameterSchema, error) {
|
||||
func (c *Client) ProjectVersionSchema(ctx context.Context, version uuid.UUID) ([]ProjectVersionParameterSchema, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s/schema", version), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -36,12 +66,12 @@ func (c *Client) ProjectVersionSchema(ctx context.Context, version uuid.UUID) ([
|
|||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var params []coderd.ProjectVersionParameterSchema
|
||||
var params []ProjectVersionParameterSchema
|
||||
return params, json.NewDecoder(res.Body).Decode(¶ms)
|
||||
}
|
||||
|
||||
// ProjectVersionParameters returns computed parameters for a project version.
|
||||
func (c *Client) ProjectVersionParameters(ctx context.Context, version uuid.UUID) ([]coderd.ProjectVersionParameter, error) {
|
||||
func (c *Client) ProjectVersionParameters(ctx context.Context, version uuid.UUID) ([]ProjectVersionParameter, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s/parameters", version), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -50,12 +80,12 @@ func (c *Client) ProjectVersionParameters(ctx context.Context, version uuid.UUID
|
|||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var params []coderd.ProjectVersionParameter
|
||||
var params []ProjectVersionParameter
|
||||
return params, json.NewDecoder(res.Body).Decode(¶ms)
|
||||
}
|
||||
|
||||
// ProjectVersionResources returns resources a project version declares.
|
||||
func (c *Client) ProjectVersionResources(ctx context.Context, version uuid.UUID) ([]coderd.WorkspaceResource, error) {
|
||||
func (c *Client) ProjectVersionResources(ctx context.Context, version uuid.UUID) ([]WorkspaceResource, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s/resources", version), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -64,16 +94,16 @@ func (c *Client) ProjectVersionResources(ctx context.Context, version uuid.UUID)
|
|||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var resources []coderd.WorkspaceResource
|
||||
var resources []WorkspaceResource
|
||||
return resources, json.NewDecoder(res.Body).Decode(&resources)
|
||||
}
|
||||
|
||||
// ProjectVersionLogsBefore returns logs that occurred before a specific time.
|
||||
func (c *Client) ProjectVersionLogsBefore(ctx context.Context, version uuid.UUID, before time.Time) ([]coderd.ProvisionerJobLog, error) {
|
||||
func (c *Client) ProjectVersionLogsBefore(ctx context.Context, version uuid.UUID, before time.Time) ([]ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsBefore(ctx, fmt.Sprintf("/api/v2/projectversions/%s/logs", version), before)
|
||||
}
|
||||
|
||||
// ProjectVersionLogsAfter streams logs for a project version that occurred after a specific time.
|
||||
func (c *Client) ProjectVersionLogsAfter(ctx context.Context, version uuid.UUID, after time.Time) (<-chan coderd.ProvisionerJobLog, error) {
|
||||
func (c *Client) ProjectVersionLogsAfter(ctx context.Context, version uuid.UUID, after time.Time) (<-chan ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsAfter(ctx, fmt.Sprintf("/api/v2/projectversions/%s/logs", version), after)
|
||||
}
|
||||
|
|
|
@ -10,15 +10,48 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/yamux"
|
||||
"golang.org/x/xerrors"
|
||||
"nhooyr.io/websocket"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/provisionerd/proto"
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
)
|
||||
|
||||
type ProvisionerDaemon database.ProvisionerDaemon
|
||||
|
||||
// ProvisionerJobStaus represents the at-time state of a job.
|
||||
type ProvisionerJobStatus string
|
||||
|
||||
const (
|
||||
ProvisionerJobPending ProvisionerJobStatus = "pending"
|
||||
ProvisionerJobRunning ProvisionerJobStatus = "running"
|
||||
ProvisionerJobSucceeded ProvisionerJobStatus = "succeeded"
|
||||
ProvisionerJobCanceling ProvisionerJobStatus = "canceling"
|
||||
ProvisionerJobCanceled ProvisionerJobStatus = "canceled"
|
||||
ProvisionerJobFailed ProvisionerJobStatus = "failed"
|
||||
)
|
||||
|
||||
type ProvisionerJob struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Status ProvisionerJobStatus `json:"status"`
|
||||
WorkerID *uuid.UUID `json:"worker_id,omitempty"`
|
||||
}
|
||||
|
||||
type ProvisionerJobLog struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Source database.LogSource `json:"log_source"`
|
||||
Level database.LogLevel `json:"log_level"`
|
||||
Output string `json:"output"`
|
||||
}
|
||||
|
||||
// ListenProvisionerDaemon returns the gRPC service for a provisioner daemon implementation.
|
||||
func (c *Client) ListenProvisionerDaemon(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
|
||||
serverURL, err := c.URL.Parse("/api/v2/provisionerdaemons/me/listen")
|
||||
|
@ -48,7 +81,7 @@ func (c *Client) ListenProvisionerDaemon(ctx context.Context) (proto.DRPCProvisi
|
|||
// provisionerJobLogsBefore provides log output that occurred before a time.
|
||||
// This is abstracted from a specific job type to provide consistency between
|
||||
// APIs. Logs is the only shared route between jobs.
|
||||
func (c *Client) provisionerJobLogsBefore(ctx context.Context, path string, before time.Time) ([]coderd.ProvisionerJobLog, error) {
|
||||
func (c *Client) provisionerJobLogsBefore(ctx context.Context, path string, before time.Time) ([]ProvisionerJobLog, error) {
|
||||
values := url.Values{}
|
||||
if !before.IsZero() {
|
||||
values["before"] = []string{strconv.FormatInt(before.UTC().UnixMilli(), 10)}
|
||||
|
@ -62,12 +95,12 @@ func (c *Client) provisionerJobLogsBefore(ctx context.Context, path string, befo
|
|||
return nil, readBodyAsError(res)
|
||||
}
|
||||
|
||||
var logs []coderd.ProvisionerJobLog
|
||||
var logs []ProvisionerJobLog
|
||||
return logs, json.NewDecoder(res.Body).Decode(&logs)
|
||||
}
|
||||
|
||||
// provisionerJobLogsAfter streams logs that occurred after a specific time.
|
||||
func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after time.Time) (<-chan coderd.ProvisionerJobLog, error) {
|
||||
func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after time.Time) (<-chan ProvisionerJobLog, error) {
|
||||
afterQuery := ""
|
||||
if !after.IsZero() {
|
||||
afterQuery = fmt.Sprintf("&after=%d", after.UTC().UnixMilli())
|
||||
|
@ -81,11 +114,11 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after
|
|||
return nil, readBodyAsError(res)
|
||||
}
|
||||
|
||||
logs := make(chan coderd.ProvisionerJobLog)
|
||||
logs := make(chan ProvisionerJobLog)
|
||||
decoder := json.NewDecoder(res.Body)
|
||||
go func() {
|
||||
defer close(logs)
|
||||
var log coderd.ProvisionerJobLog
|
||||
var log ProvisionerJobLog
|
||||
for {
|
||||
err = decoder.Decode(&log)
|
||||
if err != nil {
|
||||
|
|
|
@ -5,10 +5,68 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// User represents a user in Coder.
|
||||
type User struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
Email string `json:"email" validate:"required"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
}
|
||||
|
||||
type CreateFirstUserRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Username string `json:"username" validate:"required,username"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
Organization string `json:"organization" validate:"required,username"`
|
||||
}
|
||||
|
||||
// CreateFirstUserResponse contains IDs for newly created user info.
|
||||
type CreateFirstUserResponse struct {
|
||||
UserID string `json:"user_id"`
|
||||
OrganizationID string `json:"organization_id"`
|
||||
}
|
||||
|
||||
type CreateUserRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Username string `json:"username" validate:"required,username"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
OrganizationID string `json:"organization_id" validate:"required"`
|
||||
}
|
||||
|
||||
// LoginWithPasswordRequest enables callers to authenticate with email and password.
|
||||
type LoginWithPasswordRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
// LoginWithPasswordResponse contains a session token for the newly authenticated user.
|
||||
type LoginWithPasswordResponse struct {
|
||||
SessionToken string `json:"session_token" validate:"required"`
|
||||
}
|
||||
|
||||
// GenerateAPIKeyResponse contains an API key for a user.
|
||||
type GenerateAPIKeyResponse struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type CreateOrganizationRequest struct {
|
||||
Name string `json:"name" validate:"required,username"`
|
||||
}
|
||||
|
||||
// CreateWorkspaceRequest provides options for creating a new workspace.
|
||||
type CreateWorkspaceRequest struct {
|
||||
ProjectID uuid.UUID `json:"project_id" validate:"required"`
|
||||
Name string `json:"name" validate:"username,required"`
|
||||
// ParameterValues allows for additional parameters to be provided
|
||||
// during the initial provision.
|
||||
ParameterValues []CreateParameterRequest `json:"parameter_values"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
@ -27,35 +85,35 @@ func (c *Client) HasFirstUser(ctx context.Context) (bool, error) {
|
|||
|
||||
// CreateFirstUser attempts to create the first user on a Coder deployment.
|
||||
// This initial user has superadmin privileges. If >0 users exist, this request will fail.
|
||||
func (c *Client) CreateFirstUser(ctx context.Context, req coderd.CreateFirstUserRequest) (coderd.CreateFirstUserResponse, error) {
|
||||
func (c *Client) CreateFirstUser(ctx context.Context, req CreateFirstUserRequest) (CreateFirstUserResponse, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, "/api/v2/users/first", req)
|
||||
if err != nil {
|
||||
return coderd.CreateFirstUserResponse{}, err
|
||||
return CreateFirstUserResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.CreateFirstUserResponse{}, readBodyAsError(res)
|
||||
return CreateFirstUserResponse{}, readBodyAsError(res)
|
||||
}
|
||||
var resp coderd.CreateFirstUserResponse
|
||||
var resp CreateFirstUserResponse
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// CreateUser creates a new user.
|
||||
func (c *Client) CreateUser(ctx context.Context, req coderd.CreateUserRequest) (coderd.User, error) {
|
||||
func (c *Client) CreateUser(ctx context.Context, req CreateUserRequest) (User, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, "/api/v2/users", req)
|
||||
if err != nil {
|
||||
return coderd.User{}, err
|
||||
return User{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.User{}, readBodyAsError(res)
|
||||
return User{}, readBodyAsError(res)
|
||||
}
|
||||
var user coderd.User
|
||||
var user User
|
||||
return user, json.NewDecoder(res.Body).Decode(&user)
|
||||
}
|
||||
|
||||
// CreateAPIKey generates an API key for the user ID provided.
|
||||
func (c *Client) CreateAPIKey(ctx context.Context, id string) (*coderd.GenerateAPIKeyResponse, error) {
|
||||
func (c *Client) CreateAPIKey(ctx context.Context, id string) (*GenerateAPIKeyResponse, error) {
|
||||
if id == "" {
|
||||
id = "me"
|
||||
}
|
||||
|
@ -67,25 +125,25 @@ func (c *Client) CreateAPIKey(ctx context.Context, id string) (*coderd.GenerateA
|
|||
if res.StatusCode > http.StatusCreated {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
apiKey := &coderd.GenerateAPIKeyResponse{}
|
||||
apiKey := &GenerateAPIKeyResponse{}
|
||||
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
|
||||
}
|
||||
|
||||
// LoginWithPassword creates a session token authenticating with an email and password.
|
||||
// Call `SetSessionToken()` to apply the newly acquired token to the client.
|
||||
func (c *Client) LoginWithPassword(ctx context.Context, req coderd.LoginWithPasswordRequest) (coderd.LoginWithPasswordResponse, error) {
|
||||
func (c *Client) LoginWithPassword(ctx context.Context, req LoginWithPasswordRequest) (LoginWithPasswordResponse, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, "/api/v2/users/login", req)
|
||||
if err != nil {
|
||||
return coderd.LoginWithPasswordResponse{}, err
|
||||
return LoginWithPasswordResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.LoginWithPasswordResponse{}, readBodyAsError(res)
|
||||
return LoginWithPasswordResponse{}, readBodyAsError(res)
|
||||
}
|
||||
var resp coderd.LoginWithPasswordResponse
|
||||
var resp LoginWithPasswordResponse
|
||||
err = json.NewDecoder(res.Body).Decode(&resp)
|
||||
if err != nil {
|
||||
return coderd.LoginWithPasswordResponse{}, err
|
||||
return LoginWithPasswordResponse{}, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
@ -105,24 +163,24 @@ func (c *Client) Logout(ctx context.Context) error {
|
|||
|
||||
// User returns a user for the ID provided.
|
||||
// If the ID string is empty, the current user will be returned.
|
||||
func (c *Client) User(ctx context.Context, id string) (coderd.User, error) {
|
||||
func (c *Client) User(ctx context.Context, id string) (User, error) {
|
||||
if id == "" {
|
||||
id = "me"
|
||||
}
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", id), nil)
|
||||
if err != nil {
|
||||
return coderd.User{}, err
|
||||
return User{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode > http.StatusOK {
|
||||
return coderd.User{}, readBodyAsError(res)
|
||||
return User{}, readBodyAsError(res)
|
||||
}
|
||||
var user coderd.User
|
||||
var user User
|
||||
return user, json.NewDecoder(res.Body).Decode(&user)
|
||||
}
|
||||
|
||||
// OrganizationsByUser returns all organizations the user is a member of.
|
||||
func (c *Client) OrganizationsByUser(ctx context.Context, id string) ([]coderd.Organization, error) {
|
||||
func (c *Client) OrganizationsByUser(ctx context.Context, id string) ([]Organization, error) {
|
||||
if id == "" {
|
||||
id = "me"
|
||||
}
|
||||
|
@ -134,62 +192,62 @@ func (c *Client) OrganizationsByUser(ctx context.Context, id string) ([]coderd.O
|
|||
if res.StatusCode > http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var orgs []coderd.Organization
|
||||
var orgs []Organization
|
||||
return orgs, json.NewDecoder(res.Body).Decode(&orgs)
|
||||
}
|
||||
|
||||
func (c *Client) OrganizationByName(ctx context.Context, user, name string) (coderd.Organization, error) {
|
||||
func (c *Client) OrganizationByName(ctx context.Context, user, name string) (Organization, error) {
|
||||
if user == "" {
|
||||
user = "me"
|
||||
}
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations/%s", user, name), nil)
|
||||
if err != nil {
|
||||
return coderd.Organization{}, err
|
||||
return Organization{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.Organization{}, readBodyAsError(res)
|
||||
return Organization{}, readBodyAsError(res)
|
||||
}
|
||||
var org coderd.Organization
|
||||
var org Organization
|
||||
return org, json.NewDecoder(res.Body).Decode(&org)
|
||||
}
|
||||
|
||||
// CreateOrganization creates an organization and adds the provided user as an admin.
|
||||
func (c *Client) CreateOrganization(ctx context.Context, user string, req coderd.CreateOrganizationRequest) (coderd.Organization, error) {
|
||||
func (c *Client) CreateOrganization(ctx context.Context, user string, req CreateOrganizationRequest) (Organization, error) {
|
||||
if user == "" {
|
||||
user = "me"
|
||||
}
|
||||
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/organizations", user), req)
|
||||
if err != nil {
|
||||
return coderd.Organization{}, err
|
||||
return Organization{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.Organization{}, readBodyAsError(res)
|
||||
return Organization{}, readBodyAsError(res)
|
||||
}
|
||||
var org coderd.Organization
|
||||
var org Organization
|
||||
return org, json.NewDecoder(res.Body).Decode(&org)
|
||||
}
|
||||
|
||||
// CreateWorkspace creates a new workspace for the project specified.
|
||||
func (c *Client) CreateWorkspace(ctx context.Context, user string, request coderd.CreateWorkspaceRequest) (coderd.Workspace, error) {
|
||||
func (c *Client) CreateWorkspace(ctx context.Context, user string, request CreateWorkspaceRequest) (Workspace, error) {
|
||||
if user == "" {
|
||||
user = "me"
|
||||
}
|
||||
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/workspaces", user), request)
|
||||
if err != nil {
|
||||
return coderd.Workspace{}, err
|
||||
return Workspace{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.Workspace{}, readBodyAsError(res)
|
||||
return Workspace{}, readBodyAsError(res)
|
||||
}
|
||||
var workspace coderd.Workspace
|
||||
var workspace Workspace
|
||||
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
|
||||
}
|
||||
|
||||
// WorkspacesByUser returns all workspaces the specified user has access to.
|
||||
func (c *Client) WorkspacesByUser(ctx context.Context, user string) ([]coderd.Workspace, error) {
|
||||
func (c *Client) WorkspacesByUser(ctx context.Context, user string) ([]Workspace, error) {
|
||||
if user == "" {
|
||||
user = "me"
|
||||
}
|
||||
|
@ -201,22 +259,22 @@ func (c *Client) WorkspacesByUser(ctx context.Context, user string) ([]coderd.Wo
|
|||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var workspaces []coderd.Workspace
|
||||
var workspaces []Workspace
|
||||
return workspaces, json.NewDecoder(res.Body).Decode(&workspaces)
|
||||
}
|
||||
|
||||
func (c *Client) WorkspaceByName(ctx context.Context, user, name string) (coderd.Workspace, error) {
|
||||
func (c *Client) WorkspaceByName(ctx context.Context, user, name string) (Workspace, error) {
|
||||
if user == "" {
|
||||
user = "me"
|
||||
}
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspaces/%s", user, name), nil)
|
||||
if err != nil {
|
||||
return coderd.Workspace{}, err
|
||||
return Workspace{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.Workspace{}, readBodyAsError(res)
|
||||
return Workspace{}, readBodyAsError(res)
|
||||
}
|
||||
var workspace coderd.Workspace
|
||||
var workspace Workspace
|
||||
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
|
||||
}
|
||||
|
|
|
@ -9,26 +9,55 @@ import (
|
|||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
// WorkspaceBuild is an at-point representation of a workspace state.
|
||||
// Iterate on before/after to determine a chronological history.
|
||||
type WorkspaceBuild struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
WorkspaceID uuid.UUID `json:"workspace_id"`
|
||||
ProjectVersionID uuid.UUID `json:"project_version_id"`
|
||||
BeforeID uuid.UUID `json:"before_id"`
|
||||
AfterID uuid.UUID `json:"after_id"`
|
||||
Name string `json:"name"`
|
||||
Transition database.WorkspaceTransition `json:"transition"`
|
||||
Initiator string `json:"initiator"`
|
||||
Job ProvisionerJob `json:"job"`
|
||||
}
|
||||
|
||||
// WorkspaceBuild returns a single workspace build for a workspace.
|
||||
// If history is "", the latest version is returned.
|
||||
func (c *Client) WorkspaceBuild(ctx context.Context, id uuid.UUID) (coderd.WorkspaceBuild, error) {
|
||||
func (c *Client) WorkspaceBuild(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspacebuilds/%s", id), nil)
|
||||
if err != nil {
|
||||
return coderd.WorkspaceBuild{}, err
|
||||
return WorkspaceBuild{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.WorkspaceBuild{}, readBodyAsError(res)
|
||||
return WorkspaceBuild{}, readBodyAsError(res)
|
||||
}
|
||||
var workspaceBuild coderd.WorkspaceBuild
|
||||
var workspaceBuild WorkspaceBuild
|
||||
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
|
||||
}
|
||||
|
||||
// CancelWorkspaceBuild marks a workspace build job as canceled.
|
||||
func (c *Client) CancelWorkspaceBuild(ctx context.Context, id uuid.UUID) error {
|
||||
res, err := c.request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/workspacebuilds/%s/cancel", id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return readBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WorkspaceResourcesByBuild returns resources for a workspace build.
|
||||
func (c *Client) WorkspaceResourcesByBuild(ctx context.Context, build uuid.UUID) ([]coderd.WorkspaceResource, error) {
|
||||
func (c *Client) WorkspaceResourcesByBuild(ctx context.Context, build uuid.UUID) ([]WorkspaceResource, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspacebuilds/%s/resources", build), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -37,16 +66,16 @@ func (c *Client) WorkspaceResourcesByBuild(ctx context.Context, build uuid.UUID)
|
|||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var resources []coderd.WorkspaceResource
|
||||
var resources []WorkspaceResource
|
||||
return resources, json.NewDecoder(res.Body).Decode(&resources)
|
||||
}
|
||||
|
||||
// WorkspaceBuildLogsBefore returns logs that occurred before a specific time.
|
||||
func (c *Client) WorkspaceBuildLogsBefore(ctx context.Context, version uuid.UUID, before time.Time) ([]coderd.ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsBefore(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", version), before)
|
||||
func (c *Client) WorkspaceBuildLogsBefore(ctx context.Context, build uuid.UUID, before time.Time) ([]ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsBefore(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", build), before)
|
||||
}
|
||||
|
||||
// WorkspaceBuildLogsAfter streams logs for a workspace build that occurred after a specific time.
|
||||
func (c *Client) WorkspaceBuildLogsAfter(ctx context.Context, version uuid.UUID, after time.Time) (<-chan coderd.ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsAfter(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", version), after)
|
||||
func (c *Client) WorkspaceBuildLogsAfter(ctx context.Context, build uuid.UUID, after time.Time) (<-chan ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsAfter(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", build), after)
|
||||
}
|
||||
|
|
|
@ -8,15 +8,23 @@ import (
|
|||
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
)
|
||||
|
||||
type GoogleInstanceIdentityToken struct {
|
||||
JSONWebToken string `json:"json_web_token" validate:"required"`
|
||||
}
|
||||
|
||||
// WorkspaceAgentAuthenticateResponse is returned when an instance ID
|
||||
// has been exchanged for a session token.
|
||||
type WorkspaceAgentAuthenticateResponse struct {
|
||||
SessionToken string `json:"session_token"`
|
||||
}
|
||||
|
||||
// AuthWorkspaceGoogleInstanceIdentity uses the Google Compute Engine Metadata API to
|
||||
// fetch a signed JWT, and exchange it for a session token for a workspace agent.
|
||||
//
|
||||
// The requesting instance must be registered as a resource in the latest history for a workspace.
|
||||
func (c *Client) AuthWorkspaceGoogleInstanceIdentity(ctx context.Context, serviceAccount string, gcpClient *metadata.Client) (coderd.WorkspaceAgentAuthenticateResponse, error) {
|
||||
func (c *Client) AuthWorkspaceGoogleInstanceIdentity(ctx context.Context, serviceAccount string, gcpClient *metadata.Client) (WorkspaceAgentAuthenticateResponse, error) {
|
||||
if serviceAccount == "" {
|
||||
// This is the default name specified by Google.
|
||||
serviceAccount = "default"
|
||||
|
@ -27,18 +35,18 @@ func (c *Client) AuthWorkspaceGoogleInstanceIdentity(ctx context.Context, servic
|
|||
// "format=full" is required, otherwise the responding payload will be missing "instance_id".
|
||||
jwt, err := gcpClient.Get(fmt.Sprintf("instance/service-accounts/%s/identity?audience=coder&format=full", serviceAccount))
|
||||
if err != nil {
|
||||
return coderd.WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("get metadata identity: %w", err)
|
||||
return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("get metadata identity: %w", err)
|
||||
}
|
||||
res, err := c.request(ctx, http.MethodPost, "/api/v2/workspaceresources/auth/google-instance-identity", coderd.GoogleInstanceIdentityToken{
|
||||
res, err := c.request(ctx, http.MethodPost, "/api/v2/workspaceresources/auth/google-instance-identity", GoogleInstanceIdentityToken{
|
||||
JSONWebToken: jwt,
|
||||
})
|
||||
if err != nil {
|
||||
return coderd.WorkspaceAgentAuthenticateResponse{}, err
|
||||
return WorkspaceAgentAuthenticateResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.WorkspaceAgentAuthenticateResponse{}, readBodyAsError(res)
|
||||
return WorkspaceAgentAuthenticateResponse{}, readBodyAsError(res)
|
||||
}
|
||||
var resp coderd.WorkspaceAgentAuthenticateResponse
|
||||
var resp WorkspaceAgentAuthenticateResponse
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
|
|
@ -7,13 +7,15 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/yamux"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"golang.org/x/xerrors"
|
||||
"nhooyr.io/websocket"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpmw"
|
||||
"github.com/coder/coder/peer"
|
||||
"github.com/coder/coder/peerbroker"
|
||||
|
@ -21,16 +23,69 @@ import (
|
|||
"github.com/coder/coder/provisionersdk"
|
||||
)
|
||||
|
||||
func (c *Client) WorkspaceResource(ctx context.Context, id uuid.UUID) (coderd.WorkspaceResource, error) {
|
||||
type WorkspaceAgentStatus string
|
||||
|
||||
const (
|
||||
WorkspaceAgentWaiting WorkspaceAgentStatus = "waiting"
|
||||
WorkspaceAgentConnected WorkspaceAgentStatus = "connected"
|
||||
WorkspaceAgentDisconnected WorkspaceAgentStatus = "disconnected"
|
||||
)
|
||||
|
||||
type WorkspaceResource struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
JobID uuid.UUID `json:"job_id"`
|
||||
Transition database.WorkspaceTransition `json:"workspace_transition"`
|
||||
Address string `json:"address"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Agent *WorkspaceAgent `json:"agent,omitempty"`
|
||||
}
|
||||
|
||||
type WorkspaceAgent struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
FirstConnectedAt *time.Time `json:"first_connected_at,omitempty"`
|
||||
LastConnectedAt *time.Time `json:"last_connected_at,omitempty"`
|
||||
DisconnectedAt *time.Time `json:"disconnected_at,omitempty"`
|
||||
Status WorkspaceAgentStatus `json:"status"`
|
||||
ResourceID uuid.UUID `json:"resource_id"`
|
||||
InstanceID string `json:"instance_id,omitempty"`
|
||||
EnvironmentVariables map[string]string `json:"environment_variables"`
|
||||
StartupScript string `json:"startup_script,omitempty"`
|
||||
}
|
||||
|
||||
type WorkspaceAgentResourceMetadata struct {
|
||||
MemoryTotal uint64 `json:"memory_total"`
|
||||
DiskTotal uint64 `json:"disk_total"`
|
||||
CPUCores uint64 `json:"cpu_cores"`
|
||||
CPUModel string `json:"cpu_model"`
|
||||
CPUMhz float64 `json:"cpu_mhz"`
|
||||
}
|
||||
|
||||
type WorkspaceAgentInstanceMetadata struct {
|
||||
JailOrchestrator string `json:"jail_orchestrator"`
|
||||
OperatingSystem string `json:"operating_system"`
|
||||
Platform string `json:"platform"`
|
||||
PlatformFamily string `json:"platform_family"`
|
||||
KernelVersion string `json:"kernel_version"`
|
||||
KernelArchitecture string `json:"kernel_architecture"`
|
||||
Cloud string `json:"cloud"`
|
||||
Jail string `json:"jail"`
|
||||
VNC bool `json:"vnc"`
|
||||
}
|
||||
|
||||
func (c *Client) WorkspaceResource(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceresources/%s", id), nil)
|
||||
if err != nil {
|
||||
return coderd.WorkspaceResource{}, err
|
||||
return WorkspaceResource{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.WorkspaceResource{}, readBodyAsError(res)
|
||||
return WorkspaceResource{}, readBodyAsError(res)
|
||||
}
|
||||
var resource coderd.WorkspaceResource
|
||||
var resource WorkspaceResource
|
||||
return resource, json.NewDecoder(res.Body).Decode(&resource)
|
||||
}
|
||||
|
||||
|
@ -106,5 +161,9 @@ func (c *Client) ListenWorkspaceAgent(ctx context.Context, opts *peer.ConnOption
|
|||
if err != nil {
|
||||
return nil, xerrors.Errorf("multiplex client: %w", err)
|
||||
}
|
||||
return peerbroker.Listen(session, nil, opts)
|
||||
return peerbroker.Listen(session, func(ctx context.Context) ([]webrtc.ICEServer, error) {
|
||||
return []webrtc.ICEServer{{
|
||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||
}}, nil
|
||||
}, opts)
|
||||
}
|
||||
|
|
|
@ -5,27 +5,49 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
// Workspace is a per-user deployment of a project. It tracks
|
||||
// project versions, and can be updated.
|
||||
type Workspace struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
ProjectID uuid.UUID `json:"project_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
LatestBuild WorkspaceBuild `json:"latest_build"`
|
||||
Outdated bool `json:"outdated"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// CreateWorkspaceBuildRequest provides options to update the latest workspace build.
|
||||
type CreateWorkspaceBuildRequest struct {
|
||||
ProjectVersionID uuid.UUID `json:"project_version_id"`
|
||||
Transition database.WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"`
|
||||
DryRun bool `json:"dry_run"`
|
||||
}
|
||||
|
||||
// Workspace returns a single workspace.
|
||||
func (c *Client) Workspace(ctx context.Context, id uuid.UUID) (coderd.Workspace, error) {
|
||||
func (c *Client) Workspace(ctx context.Context, id uuid.UUID) (Workspace, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s", id), nil)
|
||||
if err != nil {
|
||||
return coderd.Workspace{}, err
|
||||
return Workspace{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.Workspace{}, readBodyAsError(res)
|
||||
return Workspace{}, readBodyAsError(res)
|
||||
}
|
||||
var workspace coderd.Workspace
|
||||
var workspace Workspace
|
||||
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
|
||||
}
|
||||
|
||||
func (c *Client) WorkspaceBuilds(ctx context.Context, workspace uuid.UUID) ([]coderd.WorkspaceBuild, error) {
|
||||
func (c *Client) WorkspaceBuilds(ctx context.Context, workspace uuid.UUID) ([]WorkspaceBuild, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds", workspace), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -34,46 +56,33 @@ func (c *Client) WorkspaceBuilds(ctx context.Context, workspace uuid.UUID) ([]co
|
|||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var workspaceBuild []coderd.WorkspaceBuild
|
||||
var workspaceBuild []WorkspaceBuild
|
||||
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
|
||||
}
|
||||
|
||||
// CreateWorkspaceBuild queues a new build to occur for a workspace.
|
||||
func (c *Client) CreateWorkspaceBuild(ctx context.Context, workspace uuid.UUID, request coderd.CreateWorkspaceBuildRequest) (coderd.WorkspaceBuild, error) {
|
||||
func (c *Client) CreateWorkspaceBuild(ctx context.Context, workspace uuid.UUID, request CreateWorkspaceBuildRequest) (WorkspaceBuild, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaces/%s/builds", workspace), request)
|
||||
if err != nil {
|
||||
return coderd.WorkspaceBuild{}, err
|
||||
return WorkspaceBuild{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.WorkspaceBuild{}, readBodyAsError(res)
|
||||
return WorkspaceBuild{}, readBodyAsError(res)
|
||||
}
|
||||
var workspaceBuild coderd.WorkspaceBuild
|
||||
var workspaceBuild WorkspaceBuild
|
||||
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
|
||||
}
|
||||
|
||||
func (c *Client) WorkspaceBuildByName(ctx context.Context, workspace uuid.UUID, name string) (coderd.WorkspaceBuild, error) {
|
||||
func (c *Client) WorkspaceBuildByName(ctx context.Context, workspace uuid.UUID, name string) (WorkspaceBuild, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds/%s", workspace, name), nil)
|
||||
if err != nil {
|
||||
return coderd.WorkspaceBuild{}, err
|
||||
return WorkspaceBuild{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.WorkspaceBuild{}, readBodyAsError(res)
|
||||
return WorkspaceBuild{}, readBodyAsError(res)
|
||||
}
|
||||
var workspaceBuild coderd.WorkspaceBuild
|
||||
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
|
||||
}
|
||||
|
||||
func (c *Client) WorkspaceBuildLatest(ctx context.Context, workspace uuid.UUID) (coderd.WorkspaceBuild, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds/latest", workspace), nil)
|
||||
if err != nil {
|
||||
return coderd.WorkspaceBuild{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.WorkspaceBuild{}, readBodyAsError(res)
|
||||
}
|
||||
var workspaceBuild coderd.WorkspaceBuild
|
||||
var workspaceBuild WorkspaceBuild
|
||||
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ func New() database.Store {
|
|||
|
||||
// fakeQuerier replicates database functionality to enable quick testing.
|
||||
type fakeQuerier struct {
|
||||
mutex sync.Mutex
|
||||
mutex sync.RWMutex
|
||||
|
||||
// Legacy tables
|
||||
apiKeys []database.APIKey
|
||||
|
@ -108,8 +108,8 @@ func (q *fakeQuerier) DeleteParameterValueByID(_ context.Context, id uuid.UUID)
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetAPIKeyByID(_ context.Context, id string) (database.APIKey, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, apiKey := range q.apiKeys {
|
||||
if apiKey.ID == id {
|
||||
|
@ -120,8 +120,8 @@ func (q *fakeQuerier) GetAPIKeyByID(_ context.Context, id string) (database.APIK
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetFileByHash(_ context.Context, hash string) (database.File, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, file := range q.files {
|
||||
if file.Hash == hash {
|
||||
|
@ -132,8 +132,8 @@ func (q *fakeQuerier) GetFileByHash(_ context.Context, hash string) (database.Fi
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetUserByEmailOrUsername(_ context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, user := range q.users {
|
||||
if user.Email == arg.Email || user.Username == arg.Username {
|
||||
|
@ -144,8 +144,8 @@ func (q *fakeQuerier) GetUserByEmailOrUsername(_ context.Context, arg database.G
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetUserByID(_ context.Context, id string) (database.User, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, user := range q.users {
|
||||
if user.ID == id {
|
||||
|
@ -156,15 +156,35 @@ func (q *fakeQuerier) GetUserByID(_ context.Context, id string) (database.User,
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetUserCount(_ context.Context) (int64, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
return int64(len(q.users)), nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspacesByProjectID(_ context.Context, arg database.GetWorkspacesByProjectIDParams) ([]database.Workspace, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
workspaces := make([]database.Workspace, 0)
|
||||
for _, workspace := range q.workspace {
|
||||
if workspace.ProjectID.String() != arg.ProjectID.String() {
|
||||
continue
|
||||
}
|
||||
if workspace.Deleted != arg.Deleted {
|
||||
continue
|
||||
}
|
||||
workspaces = append(workspaces, workspace)
|
||||
}
|
||||
if len(workspaces) == 0 {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
return workspaces, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceByID(_ context.Context, id uuid.UUID) (database.Workspace, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, workspace := range q.workspace {
|
||||
if workspace.ID.String() == id.String() {
|
||||
|
@ -175,8 +195,8 @@ func (q *fakeQuerier) GetWorkspaceByID(_ context.Context, id uuid.UUID) (databas
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceByUserIDAndName(_ context.Context, arg database.GetWorkspaceByUserIDAndNameParams) (database.Workspace, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, workspace := range q.workspace {
|
||||
if workspace.OwnerID != arg.OwnerID {
|
||||
|
@ -185,12 +205,18 @@ func (q *fakeQuerier) GetWorkspaceByUserIDAndName(_ context.Context, arg databas
|
|||
if !strings.EqualFold(workspace.Name, arg.Name) {
|
||||
continue
|
||||
}
|
||||
if workspace.Deleted != arg.Deleted {
|
||||
continue
|
||||
}
|
||||
return workspace, nil
|
||||
}
|
||||
return database.Workspace{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceOwnerCountsByProjectIDs(_ context.Context, projectIDs []uuid.UUID) ([]database.GetWorkspaceOwnerCountsByProjectIDsRow, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
counts := map[string]map[string]struct{}{}
|
||||
for _, projectID := range projectIDs {
|
||||
found := false
|
||||
|
@ -198,6 +224,9 @@ func (q *fakeQuerier) GetWorkspaceOwnerCountsByProjectIDs(_ context.Context, pro
|
|||
if workspace.ProjectID.String() != projectID.String() {
|
||||
continue
|
||||
}
|
||||
if workspace.Deleted {
|
||||
continue
|
||||
}
|
||||
countByOwnerID, ok := counts[projectID.String()]
|
||||
if !ok {
|
||||
countByOwnerID = map[string]struct{}{}
|
||||
|
@ -226,8 +255,8 @@ func (q *fakeQuerier) GetWorkspaceOwnerCountsByProjectIDs(_ context.Context, pro
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceBuildByID(_ context.Context, id uuid.UUID) (database.WorkspaceBuild, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, history := range q.workspaceBuild {
|
||||
if history.ID.String() == id.String() {
|
||||
|
@ -238,8 +267,8 @@ func (q *fakeQuerier) GetWorkspaceBuildByID(_ context.Context, id uuid.UUID) (da
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUID) (database.WorkspaceBuild, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, build := range q.workspaceBuild {
|
||||
if build.JobID.String() == jobID.String() {
|
||||
|
@ -250,8 +279,8 @@ func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUI
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDWithoutAfter(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, workspaceBuild := range q.workspaceBuild {
|
||||
if workspaceBuild.WorkspaceID.String() != workspaceID.String() {
|
||||
|
@ -264,9 +293,28 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDWithoutAfter(_ context.Conte
|
|||
return database.WorkspaceBuild{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceBuildsByWorkspaceIDsWithoutAfter(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceBuild, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
builds := make([]database.WorkspaceBuild, 0)
|
||||
for _, workspaceBuild := range q.workspaceBuild {
|
||||
for _, id := range ids {
|
||||
if id.String() != workspaceBuild.WorkspaceID.String() {
|
||||
continue
|
||||
}
|
||||
builds = append(builds, workspaceBuild)
|
||||
}
|
||||
}
|
||||
if len(builds) == 0 {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
return builds, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceID(_ context.Context, workspaceID uuid.UUID) ([]database.WorkspaceBuild, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
history := make([]database.WorkspaceBuild, 0)
|
||||
for _, workspaceBuild := range q.workspaceBuild {
|
||||
|
@ -281,8 +329,8 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceID(_ context.Context, workspac
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndName(_ context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndNameParams) (database.WorkspaceBuild, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, workspaceBuild := range q.workspaceBuild {
|
||||
if workspaceBuild.WorkspaceID.String() != arg.WorkspaceID.String() {
|
||||
|
@ -296,13 +344,16 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndName(_ context.Context, a
|
|||
return database.WorkspaceBuild{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspacesByUserID(_ context.Context, ownerID string) ([]database.Workspace, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
func (q *fakeQuerier) GetWorkspacesByUserID(_ context.Context, req database.GetWorkspacesByUserIDParams) ([]database.Workspace, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
workspaces := make([]database.Workspace, 0)
|
||||
for _, workspace := range q.workspace {
|
||||
if workspace.OwnerID != ownerID {
|
||||
if workspace.OwnerID != req.OwnerID {
|
||||
continue
|
||||
}
|
||||
if workspace.Deleted != req.Deleted {
|
||||
continue
|
||||
}
|
||||
workspaces = append(workspaces, workspace)
|
||||
|
@ -314,8 +365,8 @@ func (q *fakeQuerier) GetWorkspacesByUserID(_ context.Context, ownerID string) (
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetOrganizationByID(_ context.Context, id string) (database.Organization, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, organization := range q.organizations {
|
||||
if organization.ID == id {
|
||||
|
@ -326,8 +377,8 @@ func (q *fakeQuerier) GetOrganizationByID(_ context.Context, id string) (databas
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetOrganizationByName(_ context.Context, name string) (database.Organization, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, organization := range q.organizations {
|
||||
if organization.Name == name {
|
||||
|
@ -338,8 +389,8 @@ func (q *fakeQuerier) GetOrganizationByName(_ context.Context, name string) (dat
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetOrganizationsByUserID(_ context.Context, userID string) ([]database.Organization, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
organizations := make([]database.Organization, 0)
|
||||
for _, organizationMember := range q.organizationMembers {
|
||||
|
@ -360,8 +411,8 @@ func (q *fakeQuerier) GetOrganizationsByUserID(_ context.Context, userID string)
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetParameterValuesByScope(_ context.Context, arg database.GetParameterValuesByScopeParams) ([]database.ParameterValue, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
parameterValues := make([]database.ParameterValue, 0)
|
||||
for _, parameterValue := range q.parameterValue {
|
||||
|
@ -380,8 +431,8 @@ func (q *fakeQuerier) GetParameterValuesByScope(_ context.Context, arg database.
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetProjectByID(_ context.Context, id uuid.UUID) (database.Project, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, project := range q.project {
|
||||
if project.ID.String() == id.String() {
|
||||
|
@ -392,8 +443,8 @@ func (q *fakeQuerier) GetProjectByID(_ context.Context, id uuid.UUID) (database.
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetProjectByOrganizationAndName(_ context.Context, arg database.GetProjectByOrganizationAndNameParams) (database.Project, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, project := range q.project {
|
||||
if project.OrganizationID != arg.OrganizationID {
|
||||
|
@ -402,14 +453,17 @@ func (q *fakeQuerier) GetProjectByOrganizationAndName(_ context.Context, arg dat
|
|||
if !strings.EqualFold(project.Name, arg.Name) {
|
||||
continue
|
||||
}
|
||||
if project.Deleted != arg.Deleted {
|
||||
continue
|
||||
}
|
||||
return project, nil
|
||||
}
|
||||
return database.Project{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetProjectVersionsByProjectID(_ context.Context, projectID uuid.UUID) ([]database.ProjectVersion, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
version := make([]database.ProjectVersion, 0)
|
||||
for _, projectVersion := range q.projectVersion {
|
||||
|
@ -425,8 +479,8 @@ func (q *fakeQuerier) GetProjectVersionsByProjectID(_ context.Context, projectID
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetProjectVersionByProjectIDAndName(_ context.Context, arg database.GetProjectVersionByProjectIDAndNameParams) (database.ProjectVersion, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, projectVersion := range q.projectVersion {
|
||||
if projectVersion.ProjectID.UUID.String() != arg.ProjectID.UUID.String() {
|
||||
|
@ -441,8 +495,8 @@ func (q *fakeQuerier) GetProjectVersionByProjectIDAndName(_ context.Context, arg
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetProjectVersionByID(_ context.Context, projectVersionID uuid.UUID) (database.ProjectVersion, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, projectVersion := range q.projectVersion {
|
||||
if projectVersion.ID.String() != projectVersionID.String() {
|
||||
|
@ -453,9 +507,22 @@ func (q *fakeQuerier) GetProjectVersionByID(_ context.Context, projectVersionID
|
|||
return database.ProjectVersion{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetProjectVersionByJobID(_ context.Context, jobID uuid.UUID) (database.ProjectVersion, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, projectVersion := range q.projectVersion {
|
||||
if projectVersion.JobID.String() != jobID.String() {
|
||||
continue
|
||||
}
|
||||
return projectVersion, nil
|
||||
}
|
||||
return database.ProjectVersion{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetParameterSchemasByJobID(_ context.Context, jobID uuid.UUID) ([]database.ParameterSchema, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
parameters := make([]database.ParameterSchema, 0)
|
||||
for _, parameterSchema := range q.parameterSchema {
|
||||
|
@ -471,8 +538,8 @@ func (q *fakeQuerier) GetParameterSchemasByJobID(_ context.Context, jobID uuid.U
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetParameterValueByScopeAndName(_ context.Context, arg database.GetParameterValueByScopeAndNameParams) (database.ParameterValue, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, parameterValue := range q.parameterValue {
|
||||
if parameterValue.Scope != arg.Scope {
|
||||
|
@ -489,15 +556,37 @@ func (q *fakeQuerier) GetParameterValueByScopeAndName(_ context.Context, arg dat
|
|||
return database.ParameterValue{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetProjectsByOrganization(_ context.Context, organizationID string) ([]database.Project, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
func (q *fakeQuerier) GetProjectsByOrganization(_ context.Context, arg database.GetProjectsByOrganizationParams) ([]database.Project, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
projects := make([]database.Project, 0)
|
||||
for _, project := range q.project {
|
||||
if project.OrganizationID == organizationID {
|
||||
if project.Deleted != arg.Deleted {
|
||||
continue
|
||||
}
|
||||
if project.OrganizationID != arg.OrganizationID {
|
||||
continue
|
||||
}
|
||||
projects = append(projects, project)
|
||||
}
|
||||
if len(projects) == 0 {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetProjectsByIDs(_ context.Context, ids []uuid.UUID) ([]database.Project, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
projects := make([]database.Project, 0)
|
||||
for _, project := range q.project {
|
||||
for _, id := range ids {
|
||||
if project.ID.String() != id.String() {
|
||||
continue
|
||||
}
|
||||
projects = append(projects, project)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(projects) == 0 {
|
||||
|
@ -507,8 +596,8 @@ func (q *fakeQuerier) GetProjectsByOrganization(_ context.Context, organizationI
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetOrganizationMemberByUserID(_ context.Context, arg database.GetOrganizationMemberByUserIDParams) (database.OrganizationMember, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, organizationMember := range q.organizationMembers {
|
||||
if organizationMember.OrganizationID != arg.OrganizationID {
|
||||
|
@ -523,8 +612,8 @@ func (q *fakeQuerier) GetOrganizationMemberByUserID(_ context.Context, arg datab
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetProvisionerDaemons(_ context.Context) ([]database.ProvisionerDaemon, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
if len(q.provisionerDaemons) == 0 {
|
||||
return nil, sql.ErrNoRows
|
||||
|
@ -533,8 +622,8 @@ func (q *fakeQuerier) GetProvisionerDaemons(_ context.Context) ([]database.Provi
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceAgentByAuthToken(_ context.Context, authToken uuid.UUID) (database.WorkspaceAgent, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, agent := range q.provisionerJobAgent {
|
||||
if agent.AuthToken.String() == authToken.String() {
|
||||
|
@ -545,8 +634,8 @@ func (q *fakeQuerier) GetWorkspaceAgentByAuthToken(_ context.Context, authToken
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceAgentByInstanceID(_ context.Context, instanceID string) (database.WorkspaceAgent, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
// The schema sorts this by created at, so we iterate the array backwards.
|
||||
for i := len(q.provisionerJobAgent) - 1; i >= 0; i-- {
|
||||
|
@ -559,8 +648,8 @@ func (q *fakeQuerier) GetWorkspaceAgentByInstanceID(_ context.Context, instanceI
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceAgentByResourceID(_ context.Context, resourceID uuid.UUID) (database.WorkspaceAgent, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, agent := range q.provisionerJobAgent {
|
||||
if agent.ResourceID.String() == resourceID.String() {
|
||||
|
@ -571,8 +660,8 @@ func (q *fakeQuerier) GetWorkspaceAgentByResourceID(_ context.Context, resourceI
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetProvisionerDaemonByID(_ context.Context, id uuid.UUID) (database.ProvisionerDaemon, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, provisionerDaemon := range q.provisionerDaemons {
|
||||
if provisionerDaemon.ID.String() != id.String() {
|
||||
|
@ -584,8 +673,8 @@ func (q *fakeQuerier) GetProvisionerDaemonByID(_ context.Context, id uuid.UUID)
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetProvisionerJobByID(_ context.Context, id uuid.UUID) (database.ProvisionerJob, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, provisionerJob := range q.provisionerJobs {
|
||||
if provisionerJob.ID.String() != id.String() {
|
||||
|
@ -597,8 +686,8 @@ func (q *fakeQuerier) GetProvisionerJobByID(_ context.Context, id uuid.UUID) (da
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceResourceByID(_ context.Context, id uuid.UUID) (database.WorkspaceResource, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, resource := range q.provisionerJobResource {
|
||||
if resource.ID.String() == id.String() {
|
||||
|
@ -609,8 +698,8 @@ func (q *fakeQuerier) GetWorkspaceResourceByID(_ context.Context, id uuid.UUID)
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceResourcesByJobID(_ context.Context, jobID uuid.UUID) ([]database.WorkspaceResource, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
resources := make([]database.WorkspaceResource, 0)
|
||||
for _, resource := range q.provisionerJobResource {
|
||||
|
@ -626,8 +715,8 @@ func (q *fakeQuerier) GetWorkspaceResourcesByJobID(_ context.Context, jobID uuid
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetProvisionerJobsByIDs(_ context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
jobs := make([]database.ProvisionerJob, 0)
|
||||
for _, job := range q.provisionerJobs {
|
||||
|
@ -646,8 +735,8 @@ func (q *fakeQuerier) GetProvisionerJobsByIDs(_ context.Context, ids []uuid.UUID
|
|||
}
|
||||
|
||||
func (q *fakeQuerier) GetProvisionerLogsByIDBetween(_ context.Context, arg database.GetProvisionerLogsByIDBetweenParams) ([]database.ProvisionerJobLog, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
logs := make([]database.ProvisionerJobLog, 0)
|
||||
for _, jobLog := range q.provisionerJobLog {
|
||||
|
@ -909,6 +998,7 @@ func (q *fakeQuerier) InsertWorkspaceResource(_ context.Context, arg database.In
|
|||
CreatedAt: arg.CreatedAt,
|
||||
JobID: arg.JobID,
|
||||
Transition: arg.Transition,
|
||||
Address: arg.Address,
|
||||
Type: arg.Type,
|
||||
Name: arg.Name,
|
||||
AgentID: arg.AgentID,
|
||||
|
@ -992,6 +1082,36 @@ func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPI
|
|||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) UpdateProjectActiveVersionByID(_ context.Context, arg database.UpdateProjectActiveVersionByIDParams) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for index, project := range q.project {
|
||||
if project.ID.String() != arg.ID.String() {
|
||||
continue
|
||||
}
|
||||
project.ActiveVersionID = arg.ActiveVersionID
|
||||
q.project[index] = project
|
||||
return nil
|
||||
}
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) UpdateProjectDeletedByID(_ context.Context, arg database.UpdateProjectDeletedByIDParams) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for index, project := range q.project {
|
||||
if project.ID.String() != arg.ID.String() {
|
||||
continue
|
||||
}
|
||||
project.Deleted = arg.Deleted
|
||||
q.project[index] = project
|
||||
return nil
|
||||
}
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) UpdateProjectVersionByID(_ context.Context, arg database.UpdateProjectVersionByIDParams) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
@ -1024,7 +1144,7 @@ func (q *fakeQuerier) UpdateProvisionerDaemonByID(_ context.Context, arg databas
|
|||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) UpdateWorkspaceAgentByID(_ context.Context, arg database.UpdateWorkspaceAgentByIDParams) error {
|
||||
func (q *fakeQuerier) UpdateWorkspaceAgentConnectionByID(_ context.Context, arg database.UpdateWorkspaceAgentConnectionByIDParams) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
|
@ -1032,7 +1152,9 @@ func (q *fakeQuerier) UpdateWorkspaceAgentByID(_ context.Context, arg database.U
|
|||
if agent.ID.String() != arg.ID.String() {
|
||||
continue
|
||||
}
|
||||
agent.UpdatedAt = arg.UpdatedAt
|
||||
agent.FirstConnectedAt = arg.FirstConnectedAt
|
||||
agent.LastConnectedAt = arg.LastConnectedAt
|
||||
agent.DisconnectedAt = arg.DisconnectedAt
|
||||
q.provisionerJobAgent[index] = agent
|
||||
return nil
|
||||
}
|
||||
|
@ -1054,6 +1176,21 @@ func (q *fakeQuerier) UpdateProvisionerJobByID(_ context.Context, arg database.U
|
|||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) UpdateProvisionerJobWithCancelByID(_ context.Context, arg database.UpdateProvisionerJobWithCancelByIDParams) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for index, job := range q.provisionerJobs {
|
||||
if arg.ID.String() != job.ID.String() {
|
||||
continue
|
||||
}
|
||||
job.CanceledAt = arg.CanceledAt
|
||||
q.provisionerJobs[index] = job
|
||||
return nil
|
||||
}
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) UpdateProvisionerJobWithCompleteByID(_ context.Context, arg database.UpdateProvisionerJobWithCompleteByIDParams) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
@ -1064,7 +1201,7 @@ func (q *fakeQuerier) UpdateProvisionerJobWithCompleteByID(_ context.Context, ar
|
|||
}
|
||||
job.UpdatedAt = arg.UpdatedAt
|
||||
job.CompletedAt = arg.CompletedAt
|
||||
job.CancelledAt = arg.CancelledAt
|
||||
job.CanceledAt = arg.CanceledAt
|
||||
job.Error = arg.Error
|
||||
q.provisionerJobs[index] = job
|
||||
return nil
|
||||
|
@ -1088,3 +1225,18 @@ func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.U
|
|||
}
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) UpdateWorkspaceDeletedByID(_ context.Context, arg database.UpdateWorkspaceDeletedByIDParams) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for index, workspace := range q.workspace {
|
||||
if workspace.ID.String() != arg.ID.String() {
|
||||
continue
|
||||
}
|
||||
workspace.Deleted = arg.Deleted
|
||||
q.workspace[index] = workspace
|
||||
return nil
|
||||
}
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
|
|
@ -169,6 +169,7 @@ CREATE TABLE projects (
|
|||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
organization_id text NOT NULL,
|
||||
deleted boolean DEFAULT false NOT NULL,
|
||||
name character varying(64) NOT NULL,
|
||||
provisioner provisioner_type NOT NULL,
|
||||
active_version_id uuid NOT NULL
|
||||
|
@ -197,7 +198,7 @@ CREATE TABLE provisioner_jobs (
|
|||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
started_at timestamp with time zone,
|
||||
cancelled_at timestamp with time zone,
|
||||
canceled_at timestamp with time zone,
|
||||
completed_at timestamp with time zone,
|
||||
error text,
|
||||
organization_id text NOT NULL,
|
||||
|
@ -235,7 +236,10 @@ CREATE TABLE users (
|
|||
CREATE TABLE workspace_agents (
|
||||
id uuid NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
first_connected_at timestamp with time zone,
|
||||
last_connected_at timestamp with time zone,
|
||||
disconnected_at timestamp with time zone,
|
||||
resource_id uuid NOT NULL,
|
||||
auth_token uuid NOT NULL,
|
||||
auth_instance_id character varying(64),
|
||||
|
@ -265,7 +269,8 @@ CREATE TABLE workspace_resources (
|
|||
created_at timestamp with time zone NOT NULL,
|
||||
job_id uuid NOT NULL,
|
||||
transition workspace_transition NOT NULL,
|
||||
type character varying(256) NOT NULL,
|
||||
address character varying(256) NOT NULL,
|
||||
type character varying(192) NOT NULL,
|
||||
name character varying(64) NOT NULL,
|
||||
agent_id uuid
|
||||
);
|
||||
|
@ -276,6 +281,7 @@ CREATE TABLE workspaces (
|
|||
updated_at timestamp with time zone NOT NULL,
|
||||
owner_id text NOT NULL,
|
||||
project_id uuid NOT NULL,
|
||||
deleted boolean DEFAULT false NOT NULL,
|
||||
name character varying(64) NOT NULL
|
||||
);
|
||||
|
||||
|
@ -339,8 +345,9 @@ ALTER TABLE ONLY workspace_resources
|
|||
ALTER TABLE ONLY workspaces
|
||||
ADD CONSTRAINT workspaces_id_key UNIQUE (id);
|
||||
|
||||
ALTER TABLE ONLY workspaces
|
||||
ADD CONSTRAINT workspaces_owner_id_name_key UNIQUE (owner_id, name);
|
||||
CREATE UNIQUE INDEX projects_organization_id_name_idx ON projects USING btree (organization_id, name) WHERE (deleted = false);
|
||||
|
||||
CREATE UNIQUE INDEX workspaces_owner_id_name_idx ON workspaces USING btree (owner_id, name) WHERE (deleted = false);
|
||||
|
||||
ALTER TABLE ONLY parameter_schemas
|
||||
ADD CONSTRAINT parameter_schemas_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE;
|
||||
|
|
|
@ -17,6 +17,7 @@ CREATE TABLE projects (
|
|||
updated_at timestamptz NOT NULL,
|
||||
-- Projects must be scoped to an organization.
|
||||
organization_id text NOT NULL,
|
||||
deleted boolean NOT NULL DEFAULT FALSE,
|
||||
name varchar(64) NOT NULL,
|
||||
provisioner provisioner_type NOT NULL,
|
||||
-- Target's a Project Version to use for Workspaces.
|
||||
|
@ -27,6 +28,9 @@ CREATE TABLE projects (
|
|||
UNIQUE(organization_id, name)
|
||||
);
|
||||
|
||||
-- Enforces no active projects have the same name.
|
||||
CREATE UNIQUE INDEX ON projects (organization_id, name) WHERE deleted = FALSE;
|
||||
|
||||
-- Project Versions store historical project data. When a Project Version is imported,
|
||||
-- an "import" job is queued to parse parameters. A Project Version
|
||||
-- can only be used if the import job succeeds.
|
||||
|
|
|
@ -4,10 +4,13 @@ CREATE TABLE workspaces (
|
|||
updated_at timestamptz NOT NULL,
|
||||
owner_id text NOT NULL,
|
||||
project_id uuid NOT NULL REFERENCES projects (id),
|
||||
name varchar(64) NOT NULL,
|
||||
UNIQUE(owner_id, name)
|
||||
deleted boolean NOT NULL DEFAULT FALSE,
|
||||
name varchar(64) NOT NULL
|
||||
);
|
||||
|
||||
-- Enforces no active workspaces have the same name.
|
||||
CREATE UNIQUE INDEX ON workspaces (owner_id, name) WHERE deleted = FALSE;
|
||||
|
||||
CREATE TYPE workspace_transition AS ENUM (
|
||||
'start',
|
||||
'stop',
|
||||
|
|
|
@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS provisioner_jobs (
|
|||
created_at timestamptz NOT NULL,
|
||||
updated_at timestamptz NOT NULL,
|
||||
started_at timestamptz,
|
||||
cancelled_at timestamptz,
|
||||
canceled_at timestamptz,
|
||||
completed_at timestamptz,
|
||||
error text,
|
||||
organization_id text NOT NULL,
|
||||
|
@ -61,7 +61,8 @@ CREATE TABLE workspace_resources (
|
|||
created_at timestamptz NOT NULL,
|
||||
job_id uuid NOT NULL REFERENCES provisioner_jobs (id) ON DELETE CASCADE,
|
||||
transition workspace_transition NOT NULL,
|
||||
type varchar(256) NOT NULL,
|
||||
address varchar(256) NOT NULL,
|
||||
type varchar(192) NOT NULL,
|
||||
name varchar(64) NOT NULL,
|
||||
agent_id uuid
|
||||
);
|
||||
|
@ -69,7 +70,10 @@ CREATE TABLE workspace_resources (
|
|||
CREATE TABLE workspace_agents (
|
||||
id uuid NOT NULL UNIQUE,
|
||||
created_at timestamptz NOT NULL,
|
||||
updated_at timestamptz,
|
||||
updated_at timestamptz NOT NULL,
|
||||
first_connected_at timestamptz,
|
||||
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_instance_id varchar(64),
|
||||
|
|
|
@ -338,6 +338,7 @@ type Project struct {
|
|||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
OrganizationID string `db:"organization_id" json:"organization_id"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Provisioner ProvisionerType `db:"provisioner" json:"provisioner"`
|
||||
ActiveVersionID uuid.UUID `db:"active_version_id" json:"active_version_id"`
|
||||
|
@ -368,7 +369,7 @@ type ProvisionerJob struct {
|
|||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
|
||||
CancelledAt sql.NullTime `db:"cancelled_at" json:"cancelled_at"`
|
||||
CanceledAt sql.NullTime `db:"canceled_at" json:"canceled_at"`
|
||||
CompletedAt sql.NullTime `db:"completed_at" json:"completed_at"`
|
||||
Error sql.NullString `db:"error" json:"error"`
|
||||
OrganizationID string `db:"organization_id" json:"organization_id"`
|
||||
|
@ -418,13 +419,17 @@ type Workspace struct {
|
|||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
OwnerID string `db:"owner_id" json:"owner_id"`
|
||||
ProjectID uuid.UUID `db:"project_id" json:"project_id"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
Name string `db:"name" json:"name"`
|
||||
}
|
||||
|
||||
type WorkspaceAgent struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
FirstConnectedAt sql.NullTime `db:"first_connected_at" json:"first_connected_at"`
|
||||
LastConnectedAt sql.NullTime `db:"last_connected_at" json:"last_connected_at"`
|
||||
DisconnectedAt sql.NullTime `db:"disconnected_at" json:"disconnected_at"`
|
||||
ResourceID uuid.UUID `db:"resource_id" json:"resource_id"`
|
||||
AuthToken uuid.UUID `db:"auth_token" json:"auth_token"`
|
||||
AuthInstanceID sql.NullString `db:"auth_instance_id" json:"auth_instance_id"`
|
||||
|
@ -454,6 +459,7 @@ type WorkspaceResource struct {
|
|||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
JobID uuid.UUID `db:"job_id" json:"job_id"`
|
||||
Transition WorkspaceTransition `db:"transition" json:"transition"`
|
||||
Address string `db:"address" json:"address"`
|
||||
Type string `db:"type" json:"type"`
|
||||
Name string `db:"name" json:"name"`
|
||||
AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"`
|
||||
|
|
|
@ -23,9 +23,11 @@ type querier interface {
|
|||
GetProjectByID(ctx context.Context, id uuid.UUID) (Project, error)
|
||||
GetProjectByOrganizationAndName(ctx context.Context, arg GetProjectByOrganizationAndNameParams) (Project, error)
|
||||
GetProjectVersionByID(ctx context.Context, id uuid.UUID) (ProjectVersion, error)
|
||||
GetProjectVersionByJobID(ctx context.Context, jobID uuid.UUID) (ProjectVersion, error)
|
||||
GetProjectVersionByProjectIDAndName(ctx context.Context, arg GetProjectVersionByProjectIDAndNameParams) (ProjectVersion, error)
|
||||
GetProjectVersionsByProjectID(ctx context.Context, dollar_1 uuid.UUID) ([]ProjectVersion, error)
|
||||
GetProjectsByOrganization(ctx context.Context, organizationID string) ([]Project, error)
|
||||
GetProjectsByIDs(ctx context.Context, ids []uuid.UUID) ([]Project, error)
|
||||
GetProjectsByOrganization(ctx context.Context, arg GetProjectsByOrganizationParams) ([]Project, error)
|
||||
GetProvisionerDaemonByID(ctx context.Context, id uuid.UUID) (ProvisionerDaemon, error)
|
||||
GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error)
|
||||
GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error)
|
||||
|
@ -42,12 +44,14 @@ type querier interface {
|
|||
GetWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceBuild, error)
|
||||
GetWorkspaceBuildByWorkspaceIDAndName(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndNameParams) (WorkspaceBuild, error)
|
||||
GetWorkspaceBuildByWorkspaceIDWithoutAfter(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error)
|
||||
GetWorkspaceBuildsByWorkspaceIDsWithoutAfter(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error)
|
||||
GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error)
|
||||
GetWorkspaceByUserIDAndName(ctx context.Context, arg GetWorkspaceByUserIDAndNameParams) (Workspace, error)
|
||||
GetWorkspaceOwnerCountsByProjectIDs(ctx context.Context, ids []uuid.UUID) ([]GetWorkspaceOwnerCountsByProjectIDsRow, error)
|
||||
GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error)
|
||||
GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error)
|
||||
GetWorkspacesByUserID(ctx context.Context, ownerID string) ([]Workspace, error)
|
||||
GetWorkspacesByProjectID(ctx context.Context, arg GetWorkspacesByProjectIDParams) ([]Workspace, error)
|
||||
GetWorkspacesByUserID(ctx context.Context, arg GetWorkspacesByUserIDParams) ([]Workspace, error)
|
||||
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
|
||||
InsertFile(ctx context.Context, arg InsertFileParams) (File, error)
|
||||
InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error)
|
||||
|
@ -65,12 +69,16 @@ type querier interface {
|
|||
InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuild, error)
|
||||
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error)
|
||||
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
|
||||
UpdateProjectActiveVersionByID(ctx context.Context, arg UpdateProjectActiveVersionByIDParams) error
|
||||
UpdateProjectDeletedByID(ctx context.Context, arg UpdateProjectDeletedByIDParams) error
|
||||
UpdateProjectVersionByID(ctx context.Context, arg UpdateProjectVersionByIDParams) error
|
||||
UpdateProvisionerDaemonByID(ctx context.Context, arg UpdateProvisionerDaemonByIDParams) error
|
||||
UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error
|
||||
UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error
|
||||
UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error
|
||||
UpdateWorkspaceAgentByID(ctx context.Context, arg UpdateWorkspaceAgentByIDParams) error
|
||||
UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error
|
||||
UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) error
|
||||
UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error
|
||||
}
|
||||
|
||||
var _ querier = (*sqlQuerier)(nil)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
;
|
||||
|
||||
-- Acquires the lock for a single job that isn't started, completed,
|
||||
-- cancelled, and that matches an array of provisioner types.
|
||||
-- canceled, and that matches an array of provisioner types.
|
||||
--
|
||||
-- SKIP LOCKED is used to jump over locked rows. This prevents
|
||||
-- multiple provisioners from acquiring the same jobs. See:
|
||||
|
@ -25,7 +25,7 @@ WHERE
|
|||
provisioner_jobs AS nested
|
||||
WHERE
|
||||
nested.started_at IS NULL
|
||||
AND nested.cancelled_at IS NULL
|
||||
AND nested.canceled_at IS NULL
|
||||
AND nested.completed_at IS NULL
|
||||
AND nested.provisioner = ANY(@types :: provisioner_type [ ])
|
||||
ORDER BY
|
||||
|
@ -164,6 +164,14 @@ WHERE
|
|||
LIMIT
|
||||
1;
|
||||
|
||||
-- name: GetProjectsByIDs :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
projects
|
||||
WHERE
|
||||
id = ANY(@ids :: uuid [ ]);
|
||||
|
||||
-- name: GetProjectByOrganizationAndName :one
|
||||
SELECT
|
||||
*
|
||||
|
@ -171,6 +179,7 @@ FROM
|
|||
projects
|
||||
WHERE
|
||||
organization_id = @organization_id
|
||||
AND deleted = @deleted
|
||||
AND LOWER(name) = LOWER(@name)
|
||||
LIMIT
|
||||
1;
|
||||
|
@ -181,7 +190,8 @@ SELECT
|
|||
FROM
|
||||
projects
|
||||
WHERE
|
||||
organization_id = $1;
|
||||
organization_id = $1
|
||||
AND deleted = $2;
|
||||
|
||||
-- name: GetParameterSchemasByJobID :many
|
||||
SELECT
|
||||
|
@ -199,6 +209,14 @@ FROM
|
|||
WHERE
|
||||
project_id = $1 :: uuid;
|
||||
|
||||
-- name: GetProjectVersionByJobID :one
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
project_versions
|
||||
WHERE
|
||||
job_id = $1;
|
||||
|
||||
-- name: GetProjectVersionByProjectIDAndName :one
|
||||
SELECT
|
||||
*
|
||||
|
@ -250,7 +268,9 @@ SELECT
|
|||
FROM
|
||||
workspace_agents
|
||||
WHERE
|
||||
auth_token = $1;
|
||||
auth_token = $1
|
||||
ORDER BY
|
||||
created_at DESC;
|
||||
|
||||
-- name: GetWorkspaceAgentByInstanceID :one
|
||||
SELECT
|
||||
|
@ -288,13 +308,23 @@ WHERE
|
|||
LIMIT
|
||||
1;
|
||||
|
||||
-- name: GetWorkspacesByProjectID :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
project_id = $1
|
||||
AND deleted = $2;
|
||||
|
||||
-- name: GetWorkspacesByUserID :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
owner_id = $1;
|
||||
owner_id = $1
|
||||
AND deleted = $2;
|
||||
|
||||
-- name: GetWorkspaceByUserIDAndName :one
|
||||
SELECT
|
||||
|
@ -303,6 +333,7 @@ FROM
|
|||
workspaces
|
||||
WHERE
|
||||
owner_id = @owner_id
|
||||
AND deleted = @deleted
|
||||
AND LOWER(name) = LOWER(@name);
|
||||
|
||||
-- name: GetWorkspaceOwnerCountsByProjectIDs :many
|
||||
|
@ -365,6 +396,15 @@ WHERE
|
|||
LIMIT
|
||||
1;
|
||||
|
||||
-- name: GetWorkspaceBuildsByWorkspaceIDsWithoutAfter :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
workspace_builds
|
||||
WHERE
|
||||
workspace_id = ANY(@ids :: uuid [ ])
|
||||
AND after_id IS NULL;
|
||||
|
||||
-- name: GetWorkspaceResourceByID :one
|
||||
SELECT
|
||||
*
|
||||
|
@ -499,12 +539,13 @@ INSERT INTO
|
|||
created_at,
|
||||
job_id,
|
||||
transition,
|
||||
address,
|
||||
type,
|
||||
name,
|
||||
agent_id
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7) RETURNING *;
|
||||
($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *;
|
||||
|
||||
-- name: InsertProjectVersion :one
|
||||
INSERT INTO
|
||||
|
@ -660,6 +701,22 @@ SET
|
|||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: UpdateProjectActiveVersionByID :exec
|
||||
UPDATE
|
||||
projects
|
||||
SET
|
||||
active_version_id = $2
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: UpdateProjectDeletedByID :exec
|
||||
UPDATE
|
||||
projects
|
||||
SET
|
||||
deleted = $2
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: UpdateProjectVersionByID :exec
|
||||
UPDATE
|
||||
project_versions
|
||||
|
@ -686,22 +743,40 @@ SET
|
|||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: UpdateProvisionerJobWithCancelByID :exec
|
||||
UPDATE
|
||||
provisioner_jobs
|
||||
SET
|
||||
canceled_at = $2
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: UpdateProvisionerJobWithCompleteByID :exec
|
||||
UPDATE
|
||||
provisioner_jobs
|
||||
SET
|
||||
updated_at = $2,
|
||||
completed_at = $3,
|
||||
cancelled_at = $4,
|
||||
canceled_at = $4,
|
||||
error = $5
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: UpdateWorkspaceAgentByID :exec
|
||||
-- name: UpdateWorkspaceDeletedByID :exec
|
||||
UPDATE
|
||||
workspaces
|
||||
SET
|
||||
deleted = $2
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: UpdateWorkspaceAgentConnectionByID :exec
|
||||
UPDATE
|
||||
workspace_agents
|
||||
SET
|
||||
updated_at = $2
|
||||
first_connected_at = $2,
|
||||
last_connected_at = $3,
|
||||
disconnected_at = $4
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ WHERE
|
|||
provisioner_jobs AS nested
|
||||
WHERE
|
||||
nested.started_at IS NULL
|
||||
AND nested.cancelled_at IS NULL
|
||||
AND nested.canceled_at IS NULL
|
||||
AND nested.completed_at IS NULL
|
||||
AND nested.provisioner = ANY($3 :: provisioner_type [ ])
|
||||
ORDER BY
|
||||
|
@ -38,7 +38,7 @@ WHERE
|
|||
SKIP LOCKED
|
||||
LIMIT
|
||||
1
|
||||
) RETURNING id, created_at, updated_at, started_at, cancelled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, storage_source, type, input, worker_id
|
||||
) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, storage_source, type, input, worker_id
|
||||
`
|
||||
|
||||
type AcquireProvisionerJobParams struct {
|
||||
|
@ -48,7 +48,7 @@ type AcquireProvisionerJobParams struct {
|
|||
}
|
||||
|
||||
// Acquires the lock for a single job that isn't started, completed,
|
||||
// cancelled, and that matches an array of provisioner types.
|
||||
// canceled, and that matches an array of provisioner types.
|
||||
//
|
||||
// SKIP LOCKED is used to jump over locked rows. This prevents
|
||||
// multiple provisioners from acquiring the same jobs. See:
|
||||
|
@ -61,7 +61,7 @@ func (q *sqlQuerier) AcquireProvisionerJob(ctx context.Context, arg AcquireProvi
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.StartedAt,
|
||||
&i.CancelledAt,
|
||||
&i.CanceledAt,
|
||||
&i.CompletedAt,
|
||||
&i.Error,
|
||||
&i.OrganizationID,
|
||||
|
@ -417,7 +417,7 @@ func (q *sqlQuerier) GetParameterValuesByScope(ctx context.Context, arg GetParam
|
|||
|
||||
const getProjectByID = `-- name: GetProjectByID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, name, provisioner, active_version_id
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id
|
||||
FROM
|
||||
projects
|
||||
WHERE
|
||||
|
@ -434,6 +434,7 @@ func (q *sqlQuerier) GetProjectByID(ctx context.Context, id uuid.UUID) (Project,
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
&i.Provisioner,
|
||||
&i.ActiveVersionID,
|
||||
|
@ -443,29 +444,32 @@ func (q *sqlQuerier) GetProjectByID(ctx context.Context, id uuid.UUID) (Project,
|
|||
|
||||
const getProjectByOrganizationAndName = `-- name: GetProjectByOrganizationAndName :one
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, name, provisioner, active_version_id
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id
|
||||
FROM
|
||||
projects
|
||||
WHERE
|
||||
organization_id = $1
|
||||
AND LOWER(name) = LOWER($2)
|
||||
AND deleted = $2
|
||||
AND LOWER(name) = LOWER($3)
|
||||
LIMIT
|
||||
1
|
||||
`
|
||||
|
||||
type GetProjectByOrganizationAndNameParams struct {
|
||||
OrganizationID string `db:"organization_id" json:"organization_id"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
Name string `db:"name" json:"name"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetProjectByOrganizationAndName(ctx context.Context, arg GetProjectByOrganizationAndNameParams) (Project, error) {
|
||||
row := q.db.QueryRowContext(ctx, getProjectByOrganizationAndName, arg.OrganizationID, arg.Name)
|
||||
row := q.db.QueryRowContext(ctx, getProjectByOrganizationAndName, arg.OrganizationID, arg.Deleted, arg.Name)
|
||||
var i Project
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
&i.Provisioner,
|
||||
&i.ActiveVersionID,
|
||||
|
@ -498,6 +502,31 @@ func (q *sqlQuerier) GetProjectVersionByID(ctx context.Context, id uuid.UUID) (P
|
|||
return i, err
|
||||
}
|
||||
|
||||
const getProjectVersionByJobID = `-- name: GetProjectVersionByJobID :one
|
||||
SELECT
|
||||
id, project_id, organization_id, created_at, updated_at, name, description, job_id
|
||||
FROM
|
||||
project_versions
|
||||
WHERE
|
||||
job_id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetProjectVersionByJobID(ctx context.Context, jobID uuid.UUID) (ProjectVersion, error) {
|
||||
row := q.db.QueryRowContext(ctx, getProjectVersionByJobID, jobID)
|
||||
var i ProjectVersion
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProjectID,
|
||||
&i.OrganizationID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.JobID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getProjectVersionByProjectIDAndName = `-- name: GetProjectVersionByProjectIDAndName :one
|
||||
SELECT
|
||||
id, project_id, organization_id, created_at, updated_at, name, description, job_id
|
||||
|
@ -570,17 +599,17 @@ func (q *sqlQuerier) GetProjectVersionsByProjectID(ctx context.Context, dollar_1
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const getProjectsByOrganization = `-- name: GetProjectsByOrganization :many
|
||||
const getProjectsByIDs = `-- name: GetProjectsByIDs :many
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, name, provisioner, active_version_id
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id
|
||||
FROM
|
||||
projects
|
||||
WHERE
|
||||
organization_id = $1
|
||||
id = ANY($1 :: uuid [ ])
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetProjectsByOrganization(ctx context.Context, organizationID string) ([]Project, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getProjectsByOrganization, organizationID)
|
||||
func (q *sqlQuerier) GetProjectsByIDs(ctx context.Context, ids []uuid.UUID) ([]Project, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getProjectsByIDs, pq.Array(ids))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -593,6 +622,54 @@ func (q *sqlQuerier) GetProjectsByOrganization(ctx context.Context, organization
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
&i.Provisioner,
|
||||
&i.ActiveVersionID,
|
||||
); 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 getProjectsByOrganization = `-- name: GetProjectsByOrganization :many
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id
|
||||
FROM
|
||||
projects
|
||||
WHERE
|
||||
organization_id = $1
|
||||
AND deleted = $2
|
||||
`
|
||||
|
||||
type GetProjectsByOrganizationParams struct {
|
||||
OrganizationID string `db:"organization_id" json:"organization_id"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetProjectsByOrganization(ctx context.Context, arg GetProjectsByOrganizationParams) ([]Project, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getProjectsByOrganization, arg.OrganizationID, arg.Deleted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Project
|
||||
for rows.Next() {
|
||||
var i Project
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
&i.Provisioner,
|
||||
&i.ActiveVersionID,
|
||||
|
@ -672,7 +749,7 @@ func (q *sqlQuerier) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDa
|
|||
|
||||
const getProvisionerJobByID = `-- name: GetProvisionerJobByID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, started_at, cancelled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, storage_source, type, input, worker_id
|
||||
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, storage_source, type, input, worker_id
|
||||
FROM
|
||||
provisioner_jobs
|
||||
WHERE
|
||||
|
@ -687,7 +764,7 @@ func (q *sqlQuerier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (P
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.StartedAt,
|
||||
&i.CancelledAt,
|
||||
&i.CanceledAt,
|
||||
&i.CompletedAt,
|
||||
&i.Error,
|
||||
&i.OrganizationID,
|
||||
|
@ -704,7 +781,7 @@ func (q *sqlQuerier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (P
|
|||
|
||||
const getProvisionerJobsByIDs = `-- name: GetProvisionerJobsByIDs :many
|
||||
SELECT
|
||||
id, created_at, updated_at, started_at, cancelled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, storage_source, type, input, worker_id
|
||||
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, storage_source, type, input, worker_id
|
||||
FROM
|
||||
provisioner_jobs
|
||||
WHERE
|
||||
|
@ -725,7 +802,7 @@ func (q *sqlQuerier) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUI
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.StartedAt,
|
||||
&i.CancelledAt,
|
||||
&i.CanceledAt,
|
||||
&i.CompletedAt,
|
||||
&i.Error,
|
||||
&i.OrganizationID,
|
||||
|
@ -899,11 +976,13 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) {
|
|||
|
||||
const getWorkspaceAgentByAuthToken = `-- name: GetWorkspaceAgentByAuthToken :one
|
||||
SELECT
|
||||
id, created_at, updated_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata
|
||||
id, created_at, updated_at, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata
|
||||
FROM
|
||||
workspace_agents
|
||||
WHERE
|
||||
auth_token = $1
|
||||
ORDER BY
|
||||
created_at DESC
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (WorkspaceAgent, error) {
|
||||
|
@ -913,6 +992,9 @@ func (q *sqlQuerier) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken
|
|||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.FirstConnectedAt,
|
||||
&i.LastConnectedAt,
|
||||
&i.DisconnectedAt,
|
||||
&i.ResourceID,
|
||||
&i.AuthToken,
|
||||
&i.AuthInstanceID,
|
||||
|
@ -926,7 +1008,7 @@ func (q *sqlQuerier) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken
|
|||
|
||||
const getWorkspaceAgentByInstanceID = `-- name: GetWorkspaceAgentByInstanceID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata
|
||||
id, created_at, updated_at, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata
|
||||
FROM
|
||||
workspace_agents
|
||||
WHERE
|
||||
|
@ -942,6 +1024,9 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst
|
|||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.FirstConnectedAt,
|
||||
&i.LastConnectedAt,
|
||||
&i.DisconnectedAt,
|
||||
&i.ResourceID,
|
||||
&i.AuthToken,
|
||||
&i.AuthInstanceID,
|
||||
|
@ -955,7 +1040,7 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst
|
|||
|
||||
const getWorkspaceAgentByResourceID = `-- name: GetWorkspaceAgentByResourceID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata
|
||||
id, created_at, updated_at, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata
|
||||
FROM
|
||||
workspace_agents
|
||||
WHERE
|
||||
|
@ -969,6 +1054,9 @@ func (q *sqlQuerier) GetWorkspaceAgentByResourceID(ctx context.Context, resource
|
|||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.FirstConnectedAt,
|
||||
&i.LastConnectedAt,
|
||||
&i.DisconnectedAt,
|
||||
&i.ResourceID,
|
||||
&i.AuthToken,
|
||||
&i.AuthInstanceID,
|
||||
|
@ -1154,9 +1242,55 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDWithoutAfter(ctx context.Cont
|
|||
return i, err
|
||||
}
|
||||
|
||||
const getWorkspaceBuildsByWorkspaceIDsWithoutAfter = `-- name: GetWorkspaceBuildsByWorkspaceIDsWithoutAfter :many
|
||||
SELECT
|
||||
id, created_at, updated_at, workspace_id, project_version_id, name, before_id, after_id, transition, initiator, provisioner_state, job_id
|
||||
FROM
|
||||
workspace_builds
|
||||
WHERE
|
||||
workspace_id = ANY($1 :: uuid [ ])
|
||||
AND after_id IS NULL
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceIDsWithoutAfter(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspaceBuildsByWorkspaceIDsWithoutAfter, pq.Array(ids))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []WorkspaceBuild
|
||||
for rows.Next() {
|
||||
var i WorkspaceBuild
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.WorkspaceID,
|
||||
&i.ProjectVersionID,
|
||||
&i.Name,
|
||||
&i.BeforeID,
|
||||
&i.AfterID,
|
||||
&i.Transition,
|
||||
&i.Initiator,
|
||||
&i.ProvisionerState,
|
||||
&i.JobID,
|
||||
); 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 getWorkspaceByID = `-- name: GetWorkspaceByID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, project_id, name
|
||||
id, created_at, updated_at, owner_id, project_id, deleted, name
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
|
@ -1174,6 +1308,7 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp
|
|||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
&i.ProjectID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
)
|
||||
return i, err
|
||||
|
@ -1181,21 +1316,23 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp
|
|||
|
||||
const getWorkspaceByUserIDAndName = `-- name: GetWorkspaceByUserIDAndName :one
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, project_id, name
|
||||
id, created_at, updated_at, owner_id, project_id, deleted, name
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
owner_id = $1
|
||||
AND LOWER(name) = LOWER($2)
|
||||
AND deleted = $2
|
||||
AND LOWER(name) = LOWER($3)
|
||||
`
|
||||
|
||||
type GetWorkspaceByUserIDAndNameParams struct {
|
||||
OwnerID string `db:"owner_id" json:"owner_id"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
Name string `db:"name" json:"name"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceByUserIDAndName(ctx context.Context, arg GetWorkspaceByUserIDAndNameParams) (Workspace, error) {
|
||||
row := q.db.QueryRowContext(ctx, getWorkspaceByUserIDAndName, arg.OwnerID, arg.Name)
|
||||
row := q.db.QueryRowContext(ctx, getWorkspaceByUserIDAndName, arg.OwnerID, arg.Deleted, arg.Name)
|
||||
var i Workspace
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
|
@ -1203,6 +1340,7 @@ func (q *sqlQuerier) GetWorkspaceByUserIDAndName(ctx context.Context, arg GetWor
|
|||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
&i.ProjectID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
)
|
||||
return i, err
|
||||
|
@ -1251,7 +1389,7 @@ func (q *sqlQuerier) GetWorkspaceOwnerCountsByProjectIDs(ctx context.Context, id
|
|||
|
||||
const getWorkspaceResourceByID = `-- name: GetWorkspaceResourceByID :one
|
||||
SELECT
|
||||
id, created_at, job_id, transition, type, name, agent_id
|
||||
id, created_at, job_id, transition, address, type, name, agent_id
|
||||
FROM
|
||||
workspace_resources
|
||||
WHERE
|
||||
|
@ -1266,6 +1404,7 @@ func (q *sqlQuerier) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID)
|
|||
&i.CreatedAt,
|
||||
&i.JobID,
|
||||
&i.Transition,
|
||||
&i.Address,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.AgentID,
|
||||
|
@ -1275,7 +1414,7 @@ func (q *sqlQuerier) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID)
|
|||
|
||||
const getWorkspaceResourcesByJobID = `-- name: GetWorkspaceResourcesByJobID :many
|
||||
SELECT
|
||||
id, created_at, job_id, transition, type, name, agent_id
|
||||
id, created_at, job_id, transition, address, type, name, agent_id
|
||||
FROM
|
||||
workspace_resources
|
||||
WHERE
|
||||
|
@ -1296,6 +1435,7 @@ func (q *sqlQuerier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uui
|
|||
&i.CreatedAt,
|
||||
&i.JobID,
|
||||
&i.Transition,
|
||||
&i.Address,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.AgentID,
|
||||
|
@ -1313,17 +1453,23 @@ func (q *sqlQuerier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uui
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspacesByUserID = `-- name: GetWorkspacesByUserID :many
|
||||
const getWorkspacesByProjectID = `-- name: GetWorkspacesByProjectID :many
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, project_id, name
|
||||
id, created_at, updated_at, owner_id, project_id, deleted, name
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
owner_id = $1
|
||||
project_id = $1
|
||||
AND deleted = $2
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspacesByUserID(ctx context.Context, ownerID string) ([]Workspace, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspacesByUserID, ownerID)
|
||||
type GetWorkspacesByProjectIDParams struct {
|
||||
ProjectID uuid.UUID `db:"project_id" json:"project_id"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetWorkspacesByProjectID(ctx context.Context, arg GetWorkspacesByProjectIDParams) ([]Workspace, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspacesByProjectID, arg.ProjectID, arg.Deleted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1337,6 +1483,53 @@ func (q *sqlQuerier) GetWorkspacesByUserID(ctx context.Context, ownerID string)
|
|||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
&i.ProjectID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
); 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 getWorkspacesByUserID = `-- name: GetWorkspacesByUserID :many
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, project_id, deleted, name
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
owner_id = $1
|
||||
AND deleted = $2
|
||||
`
|
||||
|
||||
type GetWorkspacesByUserIDParams struct {
|
||||
OwnerID string `db:"owner_id" json:"owner_id"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetWorkspacesByUserID(ctx context.Context, arg GetWorkspacesByUserIDParams) ([]Workspace, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspacesByUserID, arg.OwnerID, arg.Deleted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Workspace
|
||||
for rows.Next() {
|
||||
var i Workspace
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
&i.ProjectID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
@ -1730,7 +1923,7 @@ INSERT INTO
|
|||
active_version_id
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, updated_at, organization_id, name, provisioner, active_version_id
|
||||
($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id
|
||||
`
|
||||
|
||||
type InsertProjectParams struct {
|
||||
|
@ -1759,6 +1952,7 @@ func (q *sqlQuerier) InsertProject(ctx context.Context, arg InsertProjectParams)
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
&i.Provisioner,
|
||||
&i.ActiveVersionID,
|
||||
|
@ -1868,7 +2062,7 @@ INSERT INTO
|
|||
input
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at, started_at, cancelled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, storage_source, type, input, worker_id
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, storage_source, type, input, worker_id
|
||||
`
|
||||
|
||||
type InsertProvisionerJobParams struct {
|
||||
|
@ -1903,7 +2097,7 @@ func (q *sqlQuerier) InsertProvisionerJob(ctx context.Context, arg InsertProvisi
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.StartedAt,
|
||||
&i.CancelledAt,
|
||||
&i.CanceledAt,
|
||||
&i.CompletedAt,
|
||||
&i.Error,
|
||||
&i.OrganizationID,
|
||||
|
@ -2051,7 +2245,7 @@ INSERT INTO
|
|||
name
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6) RETURNING id, created_at, updated_at, owner_id, project_id, name
|
||||
($1, $2, $3, $4, $5, $6) RETURNING id, created_at, updated_at, owner_id, project_id, deleted, name
|
||||
`
|
||||
|
||||
type InsertWorkspaceParams struct {
|
||||
|
@ -2079,6 +2273,7 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar
|
|||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
&i.ProjectID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
)
|
||||
return i, err
|
||||
|
@ -2099,13 +2294,13 @@ INSERT INTO
|
|||
resource_metadata
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata
|
||||
`
|
||||
|
||||
type InsertWorkspaceAgentParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
ResourceID uuid.UUID `db:"resource_id" json:"resource_id"`
|
||||
AuthToken uuid.UUID `db:"auth_token" json:"auth_token"`
|
||||
AuthInstanceID sql.NullString `db:"auth_instance_id" json:"auth_instance_id"`
|
||||
|
@ -2133,6 +2328,9 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa
|
|||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.FirstConnectedAt,
|
||||
&i.LastConnectedAt,
|
||||
&i.DisconnectedAt,
|
||||
&i.ResourceID,
|
||||
&i.AuthToken,
|
||||
&i.AuthInstanceID,
|
||||
|
@ -2216,12 +2414,13 @@ INSERT INTO
|
|||
created_at,
|
||||
job_id,
|
||||
transition,
|
||||
address,
|
||||
type,
|
||||
name,
|
||||
agent_id
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, job_id, transition, type, name, agent_id
|
||||
($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at, job_id, transition, address, type, name, agent_id
|
||||
`
|
||||
|
||||
type InsertWorkspaceResourceParams struct {
|
||||
|
@ -2229,6 +2428,7 @@ type InsertWorkspaceResourceParams struct {
|
|||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
JobID uuid.UUID `db:"job_id" json:"job_id"`
|
||||
Transition WorkspaceTransition `db:"transition" json:"transition"`
|
||||
Address string `db:"address" json:"address"`
|
||||
Type string `db:"type" json:"type"`
|
||||
Name string `db:"name" json:"name"`
|
||||
AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"`
|
||||
|
@ -2240,6 +2440,7 @@ func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWork
|
|||
arg.CreatedAt,
|
||||
arg.JobID,
|
||||
arg.Transition,
|
||||
arg.Address,
|
||||
arg.Type,
|
||||
arg.Name,
|
||||
arg.AgentID,
|
||||
|
@ -2250,6 +2451,7 @@ func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWork
|
|||
&i.CreatedAt,
|
||||
&i.JobID,
|
||||
&i.Transition,
|
||||
&i.Address,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.AgentID,
|
||||
|
@ -2291,6 +2493,44 @@ func (q *sqlQuerier) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDP
|
|||
return err
|
||||
}
|
||||
|
||||
const updateProjectActiveVersionByID = `-- name: UpdateProjectActiveVersionByID :exec
|
||||
UPDATE
|
||||
projects
|
||||
SET
|
||||
active_version_id = $2
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
type UpdateProjectActiveVersionByIDParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
ActiveVersionID uuid.UUID `db:"active_version_id" json:"active_version_id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateProjectActiveVersionByID(ctx context.Context, arg UpdateProjectActiveVersionByIDParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateProjectActiveVersionByID, arg.ID, arg.ActiveVersionID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateProjectDeletedByID = `-- name: UpdateProjectDeletedByID :exec
|
||||
UPDATE
|
||||
projects
|
||||
SET
|
||||
deleted = $2
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
type UpdateProjectDeletedByIDParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateProjectDeletedByID(ctx context.Context, arg UpdateProjectDeletedByIDParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateProjectDeletedByID, arg.ID, arg.Deleted)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateProjectVersionByID = `-- name: UpdateProjectVersionByID :exec
|
||||
UPDATE
|
||||
project_versions
|
||||
|
@ -2352,13 +2592,32 @@ func (q *sqlQuerier) UpdateProvisionerJobByID(ctx context.Context, arg UpdatePro
|
|||
return err
|
||||
}
|
||||
|
||||
const updateProvisionerJobWithCancelByID = `-- name: UpdateProvisionerJobWithCancelByID :exec
|
||||
UPDATE
|
||||
provisioner_jobs
|
||||
SET
|
||||
canceled_at = $2
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
type UpdateProvisionerJobWithCancelByIDParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
CanceledAt sql.NullTime `db:"canceled_at" json:"canceled_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateProvisionerJobWithCancelByID, arg.ID, arg.CanceledAt)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateProvisionerJobWithCompleteByID = `-- name: UpdateProvisionerJobWithCompleteByID :exec
|
||||
UPDATE
|
||||
provisioner_jobs
|
||||
SET
|
||||
updated_at = $2,
|
||||
completed_at = $3,
|
||||
cancelled_at = $4,
|
||||
canceled_at = $4,
|
||||
error = $5
|
||||
WHERE
|
||||
id = $1
|
||||
|
@ -2368,7 +2627,7 @@ type UpdateProvisionerJobWithCompleteByIDParams struct {
|
|||
ID uuid.UUID `db:"id" json:"id"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
CompletedAt sql.NullTime `db:"completed_at" json:"completed_at"`
|
||||
CancelledAt sql.NullTime `db:"cancelled_at" json:"cancelled_at"`
|
||||
CanceledAt sql.NullTime `db:"canceled_at" json:"canceled_at"`
|
||||
Error sql.NullString `db:"error" json:"error"`
|
||||
}
|
||||
|
||||
|
@ -2377,28 +2636,37 @@ func (q *sqlQuerier) UpdateProvisionerJobWithCompleteByID(ctx context.Context, a
|
|||
arg.ID,
|
||||
arg.UpdatedAt,
|
||||
arg.CompletedAt,
|
||||
arg.CancelledAt,
|
||||
arg.CanceledAt,
|
||||
arg.Error,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateWorkspaceAgentByID = `-- name: UpdateWorkspaceAgentByID :exec
|
||||
const updateWorkspaceAgentConnectionByID = `-- name: UpdateWorkspaceAgentConnectionByID :exec
|
||||
UPDATE
|
||||
workspace_agents
|
||||
SET
|
||||
updated_at = $2
|
||||
first_connected_at = $2,
|
||||
last_connected_at = $3,
|
||||
disconnected_at = $4
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
type UpdateWorkspaceAgentByIDParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"`
|
||||
type UpdateWorkspaceAgentConnectionByIDParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
FirstConnectedAt sql.NullTime `db:"first_connected_at" json:"first_connected_at"`
|
||||
LastConnectedAt sql.NullTime `db:"last_connected_at" json:"last_connected_at"`
|
||||
DisconnectedAt sql.NullTime `db:"disconnected_at" json:"disconnected_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateWorkspaceAgentByID(ctx context.Context, arg UpdateWorkspaceAgentByIDParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateWorkspaceAgentByID, arg.ID, arg.UpdatedAt)
|
||||
func (q *sqlQuerier) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateWorkspaceAgentConnectionByID,
|
||||
arg.ID,
|
||||
arg.FirstConnectedAt,
|
||||
arg.LastConnectedAt,
|
||||
arg.DisconnectedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -2429,3 +2697,22 @@ func (q *sqlQuerier) UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWor
|
|||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateWorkspaceDeletedByID = `-- name: UpdateWorkspaceDeletedByID :exec
|
||||
UPDATE
|
||||
workspaces
|
||||
SET
|
||||
deleted = $2
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
type UpdateWorkspaceDeletedByIDParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateWorkspaceDeletedByID, arg.ID, arg.Deleted)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -31,5 +31,5 @@ function create_initial_user() {
|
|||
trap 'kill 0' SIGINT
|
||||
create_initial_user &
|
||||
CODERV2_HOST=http://127.0.0.1:3000 yarn --cwd=./site dev &
|
||||
go run cmd/coder/main.go daemon
|
||||
go run cmd/coder/main.go start
|
||||
)
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
package examples
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"embed"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/parser/pageparser"
|
||||
"golang.org/x/sync/singleflight"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed */*.md
|
||||
//go:embed */*.tf
|
||||
files embed.FS
|
||||
|
||||
examples = make([]Example, 0)
|
||||
parseExamples sync.Once
|
||||
archives = singleflight.Group{}
|
||||
)
|
||||
|
||||
type Example struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Markdown string `json:"markdown"`
|
||||
}
|
||||
|
||||
// List returns all embedded examples.
|
||||
func List() ([]Example, error) {
|
||||
var returnError error
|
||||
parseExamples.Do(func() {
|
||||
dirs, err := files.ReadDir(".")
|
||||
if err != nil {
|
||||
returnError = xerrors.Errorf("read dir: %w", err)
|
||||
return
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
exampleID := dir.Name()
|
||||
// Each one of these is a example!
|
||||
readme, err := files.ReadFile(path.Join(dir.Name(), "README.md"))
|
||||
if err != nil {
|
||||
returnError = xerrors.Errorf("example %q does not contain README.md", exampleID)
|
||||
return
|
||||
}
|
||||
frontMatter, err := pageparser.ParseFrontMatterAndContent(bytes.NewReader(readme))
|
||||
if err != nil {
|
||||
returnError = xerrors.Errorf("parse example %q front matter: %w", exampleID, err)
|
||||
return
|
||||
}
|
||||
nameRaw, exists := frontMatter.FrontMatter["name"]
|
||||
if !exists {
|
||||
returnError = xerrors.Errorf("example %q front matter does not contain name", exampleID)
|
||||
return
|
||||
}
|
||||
name, valid := nameRaw.(string)
|
||||
if !valid {
|
||||
returnError = xerrors.Errorf("example %q name isn't a string", exampleID)
|
||||
return
|
||||
}
|
||||
descriptionRaw, exists := frontMatter.FrontMatter["description"]
|
||||
if !exists {
|
||||
returnError = xerrors.Errorf("example %q front matter does not contain name", exampleID)
|
||||
return
|
||||
}
|
||||
description, valid := descriptionRaw.(string)
|
||||
if !valid {
|
||||
returnError = xerrors.Errorf("example %q description isn't a string", exampleID)
|
||||
return
|
||||
}
|
||||
examples = append(examples, Example{
|
||||
ID: exampleID,
|
||||
Name: name,
|
||||
Description: description,
|
||||
Markdown: string(frontMatter.Content),
|
||||
})
|
||||
}
|
||||
})
|
||||
return examples, returnError
|
||||
}
|
||||
|
||||
// Archive returns a tar by example ID.
|
||||
func Archive(exampleID string) ([]byte, error) {
|
||||
rawData, err, _ := archives.Do(exampleID, func() (interface{}, error) {
|
||||
examples, err := List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var selected Example
|
||||
for _, example := range examples {
|
||||
if example.ID != exampleID {
|
||||
continue
|
||||
}
|
||||
selected = example
|
||||
break
|
||||
}
|
||||
if selected.ID == "" {
|
||||
return nil, xerrors.Errorf("example with id %q not found", exampleID)
|
||||
}
|
||||
|
||||
entries, err := files.ReadDir(exampleID)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("read dir: %w", err)
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
tarWriter := tar.NewWriter(&buffer)
|
||||
|
||||
for _, entry := range entries {
|
||||
file, err := files.Open(path.Join(exampleID, entry.Name()))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("open file: %w", err)
|
||||
}
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("stat file: %w", err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
continue
|
||||
}
|
||||
data := make([]byte, info.Size())
|
||||
_, err = file.Read(data)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("read data: %w", err)
|
||||
}
|
||||
header, err := tar.FileInfoHeader(info, entry.Name())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get file header: %w", err)
|
||||
}
|
||||
header.Mode = 0644
|
||||
err = tarWriter.WriteHeader(header)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("write file: %w", err)
|
||||
}
|
||||
_, err = tarWriter.Write(data)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("write: %w", err)
|
||||
}
|
||||
}
|
||||
err = tarWriter.Flush()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("flush archive: %w", err)
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, valid := rawData.([]byte)
|
||||
if !valid {
|
||||
panic("dev error: data must be a byte slice")
|
||||
}
|
||||
return data, nil
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue