Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b23a58c8fe | |||
| 11e1d99625 | |||
| 8cee1fbdba | |||
| 688ab17030 | |||
| 94e50698e7 |
@@ -27,3 +27,8 @@ coverage/
|
||||
# Build
|
||||
dist/
|
||||
site/out/
|
||||
|
||||
*.tfstate
|
||||
*.tfplan
|
||||
*.lock.hcl
|
||||
.terraform/
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
archives:
|
||||
- builds:
|
||||
- id: coder
|
||||
builds:
|
||||
- coder
|
||||
files:
|
||||
- README.md
|
||||
@@ -20,7 +21,7 @@ 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
|
||||
|
||||
Vendored
+2
-1
@@ -8,7 +8,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)",
|
||||
@@ -27,6 +27,7 @@
|
||||
]
|
||||
},
|
||||
"cSpell.words": [
|
||||
"cliui",
|
||||
"coderd",
|
||||
"coderdtest",
|
||||
"codersdk",
|
||||
|
||||
@@ -92,4 +92,8 @@ site/out:
|
||||
|
||||
snapshot:
|
||||
goreleaser release --snapshot --rm-dist
|
||||
.PHONY: snapshot
|
||||
.PHONY: snapshot
|
||||
|
||||
template/%s:
|
||||
|
||||
# Embed Terraform for each platform.
|
||||
+2
-1
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"sync"
|
||||
@@ -197,7 +198,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 {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package cliui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/charmbracelet/charm/ui/common"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var (
|
||||
Canceled = errors.New("canceled")
|
||||
|
||||
defaultStyles = common.DefaultStyles()
|
||||
)
|
||||
|
||||
type Validate func(string) error
|
||||
|
||||
// ValidateNotEmpty is a helper function to disallow empty inputs!
|
||||
func ValidateNotEmpty(s string) error {
|
||||
if s == "" {
|
||||
return errors.New("Must be provided!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Styles compose visual elements of the UI!
|
||||
var Styles = struct {
|
||||
Bold,
|
||||
Code,
|
||||
Field,
|
||||
Keyword,
|
||||
Paragraph,
|
||||
Prompt,
|
||||
FocusedPrompt,
|
||||
Logo,
|
||||
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,
|
||||
Prompt: defaultStyles.Prompt.Foreground(lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"}),
|
||||
FocusedPrompt: defaultStyles.FocusedPrompt.Foreground(lipgloss.Color("#651fff")),
|
||||
Logo: defaultStyles.Logo.SetString("Coder"),
|
||||
Wrap: defaultStyles.Wrap,
|
||||
}
|
||||
|
||||
// coder login
|
||||
@@ -0,0 +1,111 @@
|
||||
package cliui
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type ListItem struct {
|
||||
ID string
|
||||
Title string
|
||||
Description string
|
||||
}
|
||||
|
||||
type ListOptions struct {
|
||||
Title string
|
||||
Items []ListItem
|
||||
}
|
||||
|
||||
func List(cmd *cobra.Command, opts ListOptions) (string, error) {
|
||||
items := make([]list.Item, 0)
|
||||
for _, item := range opts.Items {
|
||||
items = append(items, teaItem{
|
||||
id: item.ID,
|
||||
title: item.Title,
|
||||
description: item.Description,
|
||||
})
|
||||
}
|
||||
model := list.New(items, list.NewDefaultDelegate(), 0, 0)
|
||||
model.Title = "Select Template"
|
||||
model.AdditionalShortHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select")),
|
||||
}
|
||||
}
|
||||
listModel := &listModel{
|
||||
opts: opts,
|
||||
model: model,
|
||||
}
|
||||
program := tea.NewProgram(listModel, tea.WithInput(cmd.InOrStdin()), tea.WithOutput(cmd.OutOrStdout()))
|
||||
err := program.Start()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if listModel.selected != nil {
|
||||
for _, item := range opts.Items {
|
||||
if item.ID == listModel.selected.id {
|
||||
return item.ID, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", Canceled
|
||||
}
|
||||
|
||||
// teaItem fulfills the "DefaultItem" interface which allows the title
|
||||
// and description to display!
|
||||
type teaItem struct {
|
||||
id string
|
||||
title string
|
||||
description string
|
||||
}
|
||||
|
||||
func (l teaItem) FilterValue() string { return l.title + "\n" + l.description }
|
||||
func (l teaItem) Title() string { return l.title }
|
||||
func (l teaItem) Description() string { return l.description }
|
||||
|
||||
type listModel struct {
|
||||
opts ListOptions
|
||||
model list.Model
|
||||
err string
|
||||
selected *teaItem
|
||||
}
|
||||
|
||||
func (m *listModel) Init() tea.Cmd {
|
||||
return tea.EnterAltScreen
|
||||
}
|
||||
|
||||
func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
// topGap, rightGap, bottomGap, leftGap := appStyle.GetPadding()
|
||||
m.model.SetSize(msg.Width, msg.Height)
|
||||
|
||||
case tea.KeyMsg:
|
||||
// Don't match any of the keys below if we're actively filtering.
|
||||
if m.model.FilterState() == list.Filtering {
|
||||
break
|
||||
}
|
||||
switch msg.Type {
|
||||
case tea.KeyEnter:
|
||||
item := m.model.SelectedItem().(teaItem)
|
||||
m.selected = &item
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
// This will also call our delegate's update function.
|
||||
newListModel, cmd := m.model.Update(msg)
|
||||
m.model = newListModel
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (l *listModel) View() string {
|
||||
return l.model.View()
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package cliui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type PromptOptions struct {
|
||||
Text string
|
||||
Default string
|
||||
CharLimit int
|
||||
Validate Validate
|
||||
EchoMode textinput.EchoMode
|
||||
EchoCharacter rune
|
||||
IsConfirm bool
|
||||
}
|
||||
|
||||
func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
|
||||
input := &inputModel{
|
||||
opts: opts,
|
||||
model: textinput.New(),
|
||||
}
|
||||
input.model.Prompt = opts.Text + " "
|
||||
input.model.Placeholder = opts.Default
|
||||
input.model.CharLimit = opts.CharLimit
|
||||
input.model.EchoCharacter = opts.EchoCharacter
|
||||
input.model.EchoMode = opts.EchoMode
|
||||
input.model.Focus()
|
||||
|
||||
program := tea.NewProgram(input, tea.WithInput(cmd.InOrStdin()), tea.WithOutput(cmd.OutOrStdout()))
|
||||
err := program.Start()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if input.canceled {
|
||||
return "", Canceled
|
||||
}
|
||||
if opts.IsConfirm && !strings.EqualFold(input.model.Value(), "yes") {
|
||||
return "", Canceled
|
||||
}
|
||||
return input.model.Value(), nil
|
||||
}
|
||||
|
||||
type inputModel struct {
|
||||
opts PromptOptions
|
||||
model textinput.Model
|
||||
err string
|
||||
canceled bool
|
||||
}
|
||||
|
||||
func (*inputModel) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (i *inputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.Type {
|
||||
case tea.KeyEnter:
|
||||
// Apply the default placeholder value.
|
||||
if i.model.Value() == "" && i.model.Placeholder != "" {
|
||||
i.model.SetValue(i.model.Placeholder)
|
||||
}
|
||||
|
||||
// Validate the value.
|
||||
if i.opts.Validate != nil {
|
||||
err := i.opts.Validate(i.model.Value())
|
||||
if err != nil {
|
||||
i.err = err.Error()
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
i.err = ""
|
||||
i.model.SetCursorMode(textinput.CursorHide)
|
||||
|
||||
return i, tea.Quit
|
||||
case tea.KeyCtrlC, tea.KeyEsc:
|
||||
i.canceled = true
|
||||
i.model.SetCursorMode(textinput.CursorHide)
|
||||
return i, tea.Quit
|
||||
}
|
||||
|
||||
// We handle errors just like any other message
|
||||
case error:
|
||||
i.err = msg.Error()
|
||||
return i, nil
|
||||
}
|
||||
|
||||
i.model, cmd = i.model.Update(msg)
|
||||
return i, cmd
|
||||
}
|
||||
|
||||
func (i *inputModel) View() string {
|
||||
prompt := Styles.FocusedPrompt
|
||||
if i.model.CursorMode() == textinput.CursorHide {
|
||||
prompt = Styles.Prompt
|
||||
}
|
||||
validate := ""
|
||||
if i.err != "" {
|
||||
validate = "\n" + defaultStyles.Error.Render("▲ "+i.err)
|
||||
}
|
||||
return prompt.String() + i.model.View() + "\n" + validate
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package cliui_test
|
||||
|
||||
import (
|
||||
"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)
|
||||
ch := make(chan string)
|
||||
go func() {
|
||||
resp, err := prompt(ptty, cliui.PromptOptions{
|
||||
Text: "Example",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
ch <- resp
|
||||
}()
|
||||
ptty.ExpectMatch("Example")
|
||||
ptty.WriteLine("hello")
|
||||
require.Equal(t, "hello", <-ch)
|
||||
})
|
||||
|
||||
t.Run("Confirm", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ptty := ptytest.New(t)
|
||||
ch := make(chan string, 0)
|
||||
go func() {
|
||||
resp, err := prompt(ptty, cliui.PromptOptions{
|
||||
Text: "Example",
|
||||
IsConfirm: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
ch <- resp
|
||||
}()
|
||||
ptty.ExpectMatch("Example")
|
||||
ptty.WriteLine("yes")
|
||||
require.Equal(t, "yes", <-ch)
|
||||
})
|
||||
}
|
||||
|
||||
func prompt(ptty *ptytest.PTY, opts cliui.PromptOptions) (string, error) {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.SetOutput(ptty.Output())
|
||||
cmd.SetIn(ptty.Input().Reader)
|
||||
return cliui.Prompt(cmd, opts)
|
||||
}
|
||||
+53
-12
@@ -2,6 +2,7 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@@ -12,10 +13,14 @@ import (
|
||||
|
||||
"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"
|
||||
@@ -28,29 +33,62 @@ import (
|
||||
func daemon() *cobra.Command {
|
||||
var (
|
||||
address string
|
||||
dev bool
|
||||
)
|
||||
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()
|
||||
|
||||
client := codersdk.New(accessURL)
|
||||
localURL := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: address,
|
||||
}
|
||||
accessURL := localURL
|
||||
var tunnelErr <-chan error
|
||||
if dev {
|
||||
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 login "+localURL.String())+" in a new terminal to get started.\n"))+`
|
||||
`)
|
||||
}
|
||||
|
||||
validator, err := idtoken.NewValidator(cmd.Context(), option.WithoutAuthentication())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Info(cmd.Context(), "opened tunnel", slog.F("url", accessURL.String()))
|
||||
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)
|
||||
@@ -67,6 +105,8 @@ func daemon() *cobra.Command {
|
||||
select {
|
||||
case <-cmd.Context().Done():
|
||||
return cmd.Context().Err()
|
||||
case err := <-tunnelErr:
|
||||
return err
|
||||
case err := <-errCh:
|
||||
return err
|
||||
}
|
||||
@@ -77,6 +117,7 @@ func daemon() *cobra.Command {
|
||||
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", "d", false, "Serve Coder in dev mode for tinkering.")
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
+36
-31
@@ -1,6 +1,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
@@ -9,14 +10,13 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"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 +67,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 +109,26 @@ 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") + ":",
|
||||
EchoMode: textinput.EchoPassword,
|
||||
EchoCharacter: '*',
|
||||
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 +147,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 +163,10 @@ 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:",
|
||||
EchoMode: textinput.EchoPassword,
|
||||
EchoCharacter: '*',
|
||||
Validate: func(token string) error {
|
||||
client.SessionToken = token
|
||||
_, err := client.User(cmd.Context(), "me")
|
||||
@@ -192,7 +197,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
|
||||
},
|
||||
}
|
||||
|
||||
+4
-5
@@ -28,7 +28,7 @@ func TestLogin(t *testing.T) {
|
||||
// https://github.com/mattn/go-isatty/issues/59
|
||||
root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty")
|
||||
pty := ptytest.New(t)
|
||||
root.SetIn(pty.Input())
|
||||
root.SetIn(pty.Input().Reader)
|
||||
root.SetOut(pty.Output())
|
||||
go func() {
|
||||
err := root.Execute()
|
||||
@@ -36,9 +36,8 @@ func TestLogin(t *testing.T) {
|
||||
}()
|
||||
|
||||
matches := []string{
|
||||
"first user?", "y",
|
||||
"first user?", "yes",
|
||||
"username", "testuser",
|
||||
"organization", "testorg",
|
||||
"email", "user@coder.com",
|
||||
"password", "password",
|
||||
}
|
||||
@@ -58,7 +57,7 @@ func TestLogin(t *testing.T) {
|
||||
|
||||
root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty", "--no-open")
|
||||
pty := ptytest.New(t)
|
||||
root.SetIn(pty.Input())
|
||||
root.SetIn(pty.Input().Reader)
|
||||
root.SetOut(pty.Output())
|
||||
go func() {
|
||||
err := root.Execute()
|
||||
@@ -77,7 +76,7 @@ func TestLogin(t *testing.T) {
|
||||
|
||||
root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty", "--no-open")
|
||||
pty := ptytest.New(t)
|
||||
root.SetIn(pty.Input())
|
||||
root.SetIn(pty.Input().Reader)
|
||||
root.SetOut(pty.Output())
|
||||
go func() {
|
||||
err := root.Execute()
|
||||
|
||||
+209
-101
@@ -1,25 +1,26 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/fatih/color"
|
||||
"github.com/google/uuid"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/term"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/agent"
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/peer"
|
||||
"github.com/coder/coder/peerbroker"
|
||||
"github.com/coder/coder/provisionerd"
|
||||
)
|
||||
|
||||
@@ -40,10 +41,52 @@ func projectCreate() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = prompt(cmd, &promptui.Prompt{
|
||||
Default: "y",
|
||||
|
||||
templates, err := client.Templates(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
items := make([]cliui.ListItem, 0)
|
||||
for _, template := range templates {
|
||||
items = append(items, cliui.ListItem{
|
||||
ID: template.ID,
|
||||
Title: template.Name,
|
||||
Description: template.Description,
|
||||
})
|
||||
}
|
||||
selectedItem, err := cliui.List(cmd, cliui.ListOptions{
|
||||
Title: "Select a Template",
|
||||
Items: items,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, cliui.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
var selectedTemplate codersdk.Template
|
||||
for _, template := range templates {
|
||||
if template.ID == selectedItem {
|
||||
selectedTemplate = template
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
archive, _, err := client.TemplateArchive(cmd.Context(), selectedTemplate.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
job, err := validateProjectVersionSource(cmd, client, organization, database.ProvisionerType(provisioner), archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
|
||||
Text: "Create project?",
|
||||
IsConfirm: true,
|
||||
Label: fmt.Sprintf("Set up %s in your organization?", color.New(color.FgHiCyan).Sprintf("%q", directory)),
|
||||
Default: "yes",
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, promptui.ErrAbort) {
|
||||
@@ -52,58 +95,168 @@ func projectCreate() *cobra.Command {
|
||||
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!")
|
||||
}
|
||||
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,
|
||||
project, err := client.CreateProject(cmd.Context(), organization.ID, codersdk.CreateProjectRequest{
|
||||
Name: selectedTemplate.ID,
|
||||
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
|
||||
}
|
||||
|
||||
_, _ = 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?",
|
||||
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
|
||||
Text: "Create a new workspace?",
|
||||
IsConfirm: true,
|
||||
Default: "y",
|
||||
Default: "yes",
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, promptui.ErrAbort) {
|
||||
if errors.Is(err, cliui.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
workspace, err := client.CreateWorkspace(cmd.Context(), "", codersdk.CreateWorkspaceRequest{
|
||||
ProjectID: project.ID,
|
||||
Name: selectedTemplate.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: job.ID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
|
||||
spin.Writer = cmd.OutOrStdout()
|
||||
spin.Suffix = " Building workspace..."
|
||||
err = spin.Color("fgHiGreen")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
spin.Start()
|
||||
defer spin.Stop()
|
||||
logs, err := client.WorkspaceBuildLogsAfter(cmd.Context(), build.ID, time.Time{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logBuffer := make([]codersdk.ProvisionerJobLog, 0, 64)
|
||||
for {
|
||||
log, ok := <-logs
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
logBuffer = append(logBuffer, log)
|
||||
}
|
||||
build, err = client.WorkspaceBuild(cmd.Context(), build.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if build.Job.Status != codersdk.ProvisionerJobSucceeded {
|
||||
for _, log := range logBuffer {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s %s\n", color.HiGreenString("[tf]"), log.Output)
|
||||
}
|
||||
return xerrors.New(build.Job.Error)
|
||||
}
|
||||
resources, err := client.WorkspaceResourcesByBuild(cmd.Context(), build.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var workspaceAgent *codersdk.WorkspaceAgent
|
||||
for _, resource := range resources {
|
||||
if resource.Agent != nil {
|
||||
workspaceAgent = resource.Agent
|
||||
break
|
||||
}
|
||||
}
|
||||
if workspaceAgent == nil {
|
||||
return xerrors.New("something went wrong.. no agent found")
|
||||
}
|
||||
spin.Suffix = " Waiting for agent to connect..."
|
||||
ticker := time.NewTicker(time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-cmd.Context().Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
}
|
||||
resource, err := client.WorkspaceResource(cmd.Context(), workspaceAgent.ResourceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resource.Agent.UpdatedAt.IsZero() {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
spin.Stop()
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s The %s workspace has been created!\n", caret, color.HiCyanString(project.Name))
|
||||
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
|
||||
Text: "Would you like to SSH?",
|
||||
IsConfirm: true,
|
||||
Default: "yes",
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, cliui.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
dialed, err := client.DialWorkspaceAgent(cmd.Context(), workspaceAgent.ResourceID)
|
||||
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
|
||||
}
|
||||
sshClient, err := agent.DialSSHClient(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session, err := sshClient.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state, err := term.MakeRaw(int(os.Stdin.Fd()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = term.Restore(int(os.Stdin.Fd()), state)
|
||||
}()
|
||||
width, height, err := term.GetSize(int(os.Stdin.Fd()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = session.RequestPty("xterm-256color", height, width, 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
|
||||
},
|
||||
}
|
||||
@@ -118,7 +271,7 @@ func projectCreate() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, organization coderd.Organization, provisioner database.ProvisionerType, directory string, parameters ...coderd.CreateParameterRequest) (*coderd.ProjectVersion, error) {
|
||||
func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, organization codersdk.Organization, provisioner database.ProvisionerType, archive []byte, parameters ...codersdk.CreateParameterRequest) (*codersdk.ProjectVersion, error) {
|
||||
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
|
||||
spin.Writer = cmd.OutOrStdout()
|
||||
spin.Suffix = " Uploading current directory..."
|
||||
@@ -129,17 +282,13 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o
|
||||
spin.Start()
|
||||
defer spin.Stop()
|
||||
|
||||
tarData, err := tarDirectory(directory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := client.Upload(cmd.Context(), codersdk.ContentTypeTar, tarData)
|
||||
resp, err := client.Upload(cmd.Context(), codersdk.ContentTypeTar, archive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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,
|
||||
Provisioner: provisioner,
|
||||
@@ -153,7 +302,7 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logBuffer := make([]coderd.ProvisionerJobLog, 0, 64)
|
||||
logBuffer := make([]codersdk.ProvisionerJobLog, 0, 64)
|
||||
for {
|
||||
log, ok := <-logs
|
||||
if !ok {
|
||||
@@ -177,7 +326,7 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o
|
||||
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
|
||||
}
|
||||
@@ -186,23 +335,23 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
value, err := prompt(cmd, &promptui.Prompt{
|
||||
Label: fmt.Sprintf("Enter value for %s:", color.HiCyanString(parameterSchema.Name)),
|
||||
value, err := cliui.Prompt(cmd, cliui.PromptOptions{
|
||||
Text: fmt.Sprintf("Enter value for %s:", color.HiCyanString(parameterSchema.Name)),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parameters = append(parameters, coderd.CreateParameterRequest{
|
||||
parameters = append(parameters, codersdk.CreateParameterRequest{
|
||||
Name: parameterSchema.Name,
|
||||
SourceValue: value,
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
DestinationScheme: parameterSchema.DefaultDestinationScheme,
|
||||
})
|
||||
}
|
||||
return validateProjectVersionSource(cmd, client, organization, provisioner, directory, parameters...)
|
||||
return validateProjectVersionSource(cmd, client, organization, provisioner, archive, parameters...)
|
||||
}
|
||||
|
||||
if version.Job.Status != coderd.ProvisionerJobSucceeded {
|
||||
if version.Job.Status != codersdk.ProvisionerJobSucceeded {
|
||||
for _, log := range logBuffer {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s %s\n", color.HiGreenString("[tf]"), log.Output)
|
||||
}
|
||||
@@ -217,44 +366,3 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestProjectCreate(t *testing.T) {
|
||||
clitest.SetupConfig(t, client, root)
|
||||
_ = coderdtest.NewProvisionerDaemon(t, client)
|
||||
pty := ptytest.New(t)
|
||||
cmd.SetIn(pty.Input())
|
||||
cmd.SetIn(pty.Input().Reader)
|
||||
cmd.SetOut(pty.Output())
|
||||
closeChan := make(chan struct{})
|
||||
go func() {
|
||||
@@ -37,9 +37,9 @@ func TestProjectCreate(t *testing.T) {
|
||||
}()
|
||||
|
||||
matches := []string{
|
||||
"organization?", "y",
|
||||
"organization?", "yes",
|
||||
"name?", "test-project",
|
||||
"project?", "y",
|
||||
"project?", "yes",
|
||||
"created!", "n",
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
@@ -74,7 +74,7 @@ func TestProjectCreate(t *testing.T) {
|
||||
clitest.SetupConfig(t, client, root)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
pty := ptytest.New(t)
|
||||
cmd.SetIn(pty.Input())
|
||||
cmd.SetIn(pty.Input().Reader)
|
||||
cmd.SetOut(pty.Output())
|
||||
closeChan := make(chan struct{})
|
||||
go func() {
|
||||
@@ -84,10 +84,10 @@ func TestProjectCreate(t *testing.T) {
|
||||
}()
|
||||
|
||||
matches := []string{
|
||||
"organization?", "y",
|
||||
"organization?", "yes",
|
||||
"name?", "test-project",
|
||||
"somevar", "value",
|
||||
"project?", "y",
|
||||
"project?", "yes",
|
||||
"created!", "n",
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/xlab/treeprint"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
@@ -40,8 +40,8 @@ func projects() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func displayProjectImportInfo(cmd *cobra.Command, parameterSchemas []coderd.ProjectVersionParameterSchema, parameterValues []coderd.ProjectVersionParameter, resources []coderd.WorkspaceResource) error {
|
||||
schemaByID := map[string]coderd.ProjectVersionParameterSchema{}
|
||||
func displayProjectImportInfo(cmd *cobra.Command, parameterSchemas []codersdk.ProjectVersionParameterSchema, parameterValues []codersdk.ProjectVersionParameter, resources []codersdk.WorkspaceResource) error {
|
||||
schemaByID := map[string]codersdk.ProjectVersionParameterSchema{}
|
||||
for _, schema := range parameterSchemas {
|
||||
schemaByID[schema.ID.String()] = schema
|
||||
}
|
||||
|
||||
+6
-73
@@ -1,25 +1,22 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"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 (
|
||||
@@ -69,8 +66,9 @@ func Root() *cobra.Command {
|
||||
cmd.AddCommand(projects())
|
||||
cmd.AddCommand(workspaces())
|
||||
cmd.AddCommand(users())
|
||||
cmd.AddCommand(workspaceSSH())
|
||||
|
||||
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>"
|
||||
@@ -146,68 +144,3 @@ func isTTY(cmd *cobra.Command) bool {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
+90
@@ -1 +1,91 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/coder/coder/agent"
|
||||
"github.com/coder/coder/peer"
|
||||
"github.com/coder/coder/peerbroker"
|
||||
)
|
||||
|
||||
func workspaceSSH() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "ssh",
|
||||
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
|
||||
}
|
||||
build, err := client.WorkspaceBuildLatest(cmd.Context(), workspace.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resources, err := client.WorkspaceResourcesByBuild(cmd.Context(), build.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
|
||||
}
|
||||
// Set raw
|
||||
terminal.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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+22
-17
@@ -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()
|
||||
},
|
||||
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -33,8 +34,8 @@ func workspaceCreate() *cobra.Command {
|
||||
if len(args) >= 2 {
|
||||
name = args[1]
|
||||
} else {
|
||||
name, err = prompt(cmd, &promptui.Prompt{
|
||||
Label: "What's your workspace's name?",
|
||||
name, err = cliui.Prompt(cmd, cliui.PromptOptions{
|
||||
Text: "What's your workspace's name?",
|
||||
Validate: func(s string) error {
|
||||
if s == "" {
|
||||
return xerrors.Errorf("You must provide a name!")
|
||||
@@ -81,9 +82,9 @@ func workspaceCreate() *cobra.Command {
|
||||
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(name)),
|
||||
Default: "yes",
|
||||
IsConfirm: true,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -93,14 +94,14 @@ func workspaceCreate() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
workspace, err := client.CreateWorkspace(cmd.Context(), "", coderd.CreateWorkspaceRequest{
|
||||
workspace, err := client.CreateWorkspace(cmd.Context(), "", codersdk.CreateWorkspaceRequest{
|
||||
ProjectID: project.ID,
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
version, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: projectVersion.ID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestWorkspaceCreate(t *testing.T) {
|
||||
clitest.SetupConfig(t, client, root)
|
||||
|
||||
pty := ptytest.New(t)
|
||||
cmd.SetIn(pty.Input())
|
||||
cmd.SetIn(pty.Input().Reader)
|
||||
cmd.SetOut(pty.Output())
|
||||
closeChan := make(chan struct{})
|
||||
go func() {
|
||||
@@ -49,7 +49,7 @@ func TestWorkspaceCreate(t *testing.T) {
|
||||
|
||||
matches := []string{
|
||||
"name?", "workspace-name",
|
||||
"Create workspace", "y",
|
||||
"Create workspace", "yes",
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
match := matches[i]
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
root := &cobra.Command{
|
||||
Use: "cliui",
|
||||
Short: "Used for visually testing UI components for the CLI.",
|
||||
}
|
||||
|
||||
root.AddCommand(&cobra.Command{
|
||||
Use: "list",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cliui.List(cmd, cliui.ListOptions{
|
||||
Items: []cliui.ListItem{{
|
||||
Title: "Example",
|
||||
Description: "Something...",
|
||||
}, {
|
||||
Title: "Wow, here's another!",
|
||||
Description: "Another exciting description!",
|
||||
}},
|
||||
})
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
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 errors.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
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
err := root.Execute()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"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"
|
||||
|
||||
"github.com/gohugoio/hugo/parser/pageparser"
|
||||
)
|
||||
|
||||
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, args, 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, args []string, 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)
|
||||
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{
|
||||
ProjectID: nil,
|
||||
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)
|
||||
}
|
||||
|
||||
schemas, err := client.ProjectVersionSchema(cmd.Context(), version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resources, err := client.ProjectVersionResources(cmd.Context(), version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
readme, err := os.OpenFile(filepath.Base(dir)+".md", os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer readme.Close()
|
||||
frontMatter, err := pageparser.ParseFrontMatterAndContent(readme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, exists := frontMatter.FrontMatter["name"]
|
||||
if !exists {
|
||||
return xerrors.New("front matter must contain name")
|
||||
}
|
||||
description, exists := frontMatter.FrontMatter["description"]
|
||||
if !exists {
|
||||
return xerrors.Errorf("front matter must contain description")
|
||||
}
|
||||
|
||||
for index, resource := range resources {
|
||||
resource.ID = uuid.UUID{}
|
||||
resource.JobID = uuid.UUID{}
|
||||
resource.CreatedAt = time.Time{}
|
||||
|
||||
if resource.Agent != nil {
|
||||
resource.Agent.ID = uuid.UUID{}
|
||||
resource.Agent.ResourceID = uuid.UUID{}
|
||||
resource.Agent.CreatedAt = time.Time{}
|
||||
}
|
||||
resources[index] = resource
|
||||
}
|
||||
|
||||
for index, schema := range schemas {
|
||||
schema.ID = uuid.UUID{}
|
||||
schema.JobID = uuid.UUID{}
|
||||
schema.CreatedAt = time.Time{}
|
||||
schemas[index] = schema
|
||||
}
|
||||
sort.Slice(resources, func(i, j int) bool {
|
||||
return resources[i].Name < resources[j].Name
|
||||
})
|
||||
sort.Slice(schemas, func(i, j int) bool {
|
||||
return schemas[i].Name < schemas[j].Name
|
||||
})
|
||||
|
||||
template := codersdk.Template{
|
||||
ID: filepath.Base(dir),
|
||||
Name: name.(string),
|
||||
Description: description.(string),
|
||||
ProjectVersionParameterSchema: schemas,
|
||||
Resources: resources,
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(template, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.WriteFile(filepath.Base(dir)+".json", data, 0600)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: version.ID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
resources, err = client.WorkspaceResourcesByBuild(cmd.Context(), build.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.UpdatedAt.IsZero() {
|
||||
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
|
||||
}
|
||||
@@ -99,6 +99,10 @@ func New(options *Options) (http.Handler, func()) {
|
||||
r.Get("/listen", api.provisionerDaemonsListen)
|
||||
})
|
||||
})
|
||||
r.Route("/templates", func(r chi.Router) {
|
||||
r.Get("/", api.listTemplates)
|
||||
r.Get("/{id}", api.templateArchive)
|
||||
})
|
||||
r.Route("/users", func(r chi.Router) {
|
||||
r.Get("/first", api.firstUser)
|
||||
r.Post("/first", api.postFirstUser)
|
||||
|
||||
@@ -134,8 +134,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 +144,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 +155,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 +164,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 +178,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 +194,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 +204,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 +216,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 +228,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)
|
||||
@@ -249,8 +249,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(),
|
||||
})
|
||||
|
||||
@@ -8,8 +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"
|
||||
)
|
||||
|
||||
@@ -26,7 +26,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{
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
|
||||
+3
-7
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
+6
-40
@@ -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,7 +46,7 @@ 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
|
||||
}
|
||||
@@ -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(),
|
||||
@@ -338,8 +304,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,7 +39,7 @@ 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{
|
||||
_, err := client.CreateProjectVersion(context.Background(), user.OrganizationID, codersdk.CreateProjectVersionRequest{
|
||||
ProjectID: &projectID,
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: "hash",
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
+10
-39
@@ -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{
|
||||
|
||||
+10
-11
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
+6
-20
@@ -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)
|
||||
@@ -81,7 +67,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 +116,8 @@ 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 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 +135,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,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)
|
||||
@@ -138,8 +121,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,
|
||||
|
||||
@@ -30,8 +30,6 @@ import (
|
||||
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)
|
||||
|
||||
+12
-40
@@ -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))
|
||||
@@ -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,
|
||||
@@ -275,20 +247,20 @@ func convertProvisionerJob(provisionerJob database.ProvisionerJob) ProvisionerJo
|
||||
|
||||
switch {
|
||||
case provisionerJob.CancelledAt.Valid:
|
||||
job.Status = ProvisionerJobCancelled
|
||||
job.Status = codersdk.ProvisionerJobCancelled
|
||||
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,8 +7,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"
|
||||
@@ -40,7 +40,7 @@ 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{
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
@@ -83,7 +83,7 @@ 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{
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
@@ -121,7 +121,7 @@ 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{
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/coder/coder/httpapi"
|
||||
"github.com/coder/coder/template"
|
||||
)
|
||||
|
||||
func (api *api) listTemplates(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(rw, r, template.List())
|
||||
}
|
||||
|
||||
func (api *api) templateArchive(rw http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
archive, exists := template.Archive(id)
|
||||
if !exists {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("template does not exists with id %q", id),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "application/x-tar")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_, _ = rw.Write(archive)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package coderd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
)
|
||||
|
||||
func TestListTemplates(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
templates, err := client.Templates(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(templates), 0)
|
||||
}
|
||||
|
||||
func TestTemplateArchive(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
templates, err := client.Templates(context.Background())
|
||||
require.NoError(t, err)
|
||||
data, _, err := client.TemplateArchive(context.Background(), templates[0].ID)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(data), 0)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
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/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// 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
|
||||
resp, err := client.Post("https://api.trycloudflare.com/tunnel", "application/json", nil)
|
||||
if err != nil {
|
||||
return "", nil, errors.Wrap(err, "failed to request quick Tunnel")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var data quickTunnelResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return "", nil, errors.Wrap(err, "failed to unmarshal quick Tunnel")
|
||||
}
|
||||
|
||||
tunnelID, err := uuid.Parse(data.Result.ID)
|
||||
if err != nil {
|
||||
return "", nil, errors.Wrap(err, "failed to parse quick Tunnel ID")
|
||||
}
|
||||
|
||||
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,51 @@
|
||||
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)
|
||||
|
||||
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)
|
||||
return res.StatusCode == http.StatusOK
|
||||
}, time.Minute, 3*time.Second)
|
||||
}
|
||||
+13
-66
@@ -14,66 +14,13 @@ import (
|
||||
"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 +43,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 +115,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 +125,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 +246,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 +294,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 +348,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 +418,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 +464,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 +483,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
|
||||
}
|
||||
@@ -637,7 +584,7 @@ func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
apiWorkspaces := make([]Workspace, 0, len(workspaces))
|
||||
apiWorkspaces := make([]codersdk.Workspace, 0, len(workspaces))
|
||||
for _, workspace := range workspaces {
|
||||
apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace))
|
||||
}
|
||||
@@ -684,8 +631,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,
|
||||
|
||||
+20
-21
@@ -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",
|
||||
})
|
||||
@@ -355,7 +354,7 @@ func TestPostWorkspacesByUser(t *testing.T) {
|
||||
job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, job.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,
|
||||
})
|
||||
|
||||
@@ -3,32 +3,15 @@ package coderd
|
||||
import (
|
||||
"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)
|
||||
@@ -66,9 +49,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,11 +63,11 @@ 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,
|
||||
|
||||
@@ -8,7 +8,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"
|
||||
@@ -25,7 +24,7 @@ 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{
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
@@ -46,7 +45,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{
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
@@ -84,7 +83,7 @@ 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{
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
@@ -136,7 +135,7 @@ 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{
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
|
||||
@@ -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,7 +19,6 @@ 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"
|
||||
@@ -69,8 +68,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,7 +105,7 @@ 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{
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
@@ -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, "")
|
||||
}()
|
||||
@@ -216,15 +188,15 @@ func (api *api) workspaceAgentListen(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func convertWorkspaceAgent(agent database.WorkspaceAgent) (WorkspaceAgent, error) {
|
||||
func convertWorkspaceAgent(agent database.WorkspaceAgent) (codersdk.WorkspaceAgent, error) {
|
||||
var envs map[string]string
|
||||
if agent.EnvironmentVariables.Valid {
|
||||
err := json.Unmarshal(agent.EnvironmentVariables.RawMessage, &envs)
|
||||
if err != nil {
|
||||
return WorkspaceAgent{}, xerrors.Errorf("unmarshal: %w", err)
|
||||
return codersdk.WorkspaceAgent{}, xerrors.Errorf("unmarshal: %w", err)
|
||||
}
|
||||
}
|
||||
return WorkspaceAgent{
|
||||
return codersdk.WorkspaceAgent{
|
||||
ID: agent.ID,
|
||||
CreatedAt: agent.CreatedAt,
|
||||
UpdatedAt: agent.UpdatedAt.Time,
|
||||
|
||||
@@ -11,7 +11,6 @@ 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"
|
||||
@@ -48,7 +47,7 @@ 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{
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
@@ -90,7 +89,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{
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
|
||||
+9
-17
@@ -13,21 +13,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"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
render.Status(r, http.StatusOK)
|
||||
@@ -57,7 +48,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,7 +67,7 @@ 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
|
||||
}
|
||||
@@ -106,17 +97,17 @@ 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),
|
||||
})
|
||||
return
|
||||
case ProvisionerJobCancelled:
|
||||
case codersdk.ProvisionerJobCancelled:
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: "The provided project version was canceled during import. You cannot create workspaces using it!",
|
||||
})
|
||||
@@ -189,6 +180,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,
|
||||
@@ -284,6 +276,6 @@ 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) codersdk.Workspace {
|
||||
return codersdk.Workspace(workspace)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -36,7 +35,7 @@ func TestPostWorkspaceBuild(t *testing.T) {
|
||||
job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, job.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,
|
||||
})
|
||||
@@ -57,7 +56,7 @@ 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{
|
||||
_, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
@@ -78,12 +77,12 @@ 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{
|
||||
_, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.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,13 +101,13 @@ 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{
|
||||
firstBuild, 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{
|
||||
secondBuild, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
@@ -147,7 +146,7 @@ func TestWorkspaceBuildLatest(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)
|
||||
_, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
|
||||
_, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
@@ -183,7 +182,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{
|
||||
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
ProjectVersionID: project.ActiveVersionID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
|
||||
+9
-6
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+58
-21
@@ -5,25 +5,62 @@ 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"`
|
||||
}
|
||||
|
||||
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 +69,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 +112,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)
|
||||
}
|
||||
|
||||
+40
-8
@@ -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)
|
||||
}
|
||||
|
||||
+26
-11
@@ -5,28 +5,43 @@ 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"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 +50,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)
|
||||
}
|
||||
|
||||
+30
-13
@@ -9,25 +9,42 @@ 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)
|
||||
}
|
||||
|
||||
// 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 +53,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 +67,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 +81,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,47 @@ 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"
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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 +80,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 +94,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 +113,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 {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package codersdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Template struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
|
||||
ProjectVersionParameterSchema []ProjectVersionParameterSchema `json:"schema"`
|
||||
Resources []WorkspaceResource `json:"resources"`
|
||||
}
|
||||
|
||||
func (c *Client) Templates(ctx context.Context) ([]Template, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, "/api/v2/templates", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var templates []Template
|
||||
return templates, json.NewDecoder(res.Body).Decode(&templates)
|
||||
}
|
||||
|
||||
func (c *Client) TemplateArchive(ctx context.Context, id string) ([]byte, string, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s", id), nil)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, "", readBodyAsError(res)
|
||||
}
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return data, res.Header.Get("Content-Type"), nil
|
||||
}
|
||||
+95
-40
@@ -5,10 +5,65 @@ 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"`
|
||||
}
|
||||
|
||||
// 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 +82,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 +122,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 +160,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 +189,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 +256,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,42 @@ 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)
|
||||
}
|
||||
|
||||
// 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 +53,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) {
|
||||
func (c *Client) WorkspaceBuildLogsBefore(ctx context.Context, version uuid.UUID, before time.Time) ([]ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsBefore(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", version), 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) {
|
||||
func (c *Client) WorkspaceBuildLogsAfter(ctx context.Context, version uuid.UUID, after time.Time) (<-chan ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsAfter(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", version), 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,56 @@ import (
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
)
|
||||
|
||||
func (c *Client) WorkspaceResource(ctx context.Context, id uuid.UUID) (coderd.WorkspaceResource, error) {
|
||||
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 (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 +148,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)
|
||||
}
|
||||
|
||||
+29
-19
@@ -8,24 +8,34 @@ import (
|
||||
|
||||
"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 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"`
|
||||
}
|
||||
|
||||
// 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 +44,46 @@ 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
|
||||
var workspaceBuild WorkspaceBuild
|
||||
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
|
||||
}
|
||||
|
||||
func (c *Client) WorkspaceBuildLatest(ctx context.Context, workspace uuid.UUID) (coderd.WorkspaceBuild, error) {
|
||||
func (c *Client) WorkspaceBuildLatest(ctx context.Context, workspace uuid.UUID) (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
|
||||
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)
|
||||
}
|
||||
|
||||
+3
-1
@@ -250,7 +250,9 @@ SELECT
|
||||
FROM
|
||||
workspace_agent
|
||||
WHERE
|
||||
auth_token = $1;
|
||||
auth_token = $1
|
||||
ORDER BY
|
||||
created_at DESC;
|
||||
|
||||
-- name: GetWorkspaceAgentByInstanceID :one
|
||||
SELECT
|
||||
|
||||
@@ -904,6 +904,8 @@ FROM
|
||||
workspace_agent
|
||||
WHERE
|
||||
auth_token = $1
|
||||
ORDER BY
|
||||
created_at DESC
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (WorkspaceAgent, error) {
|
||||
|
||||
@@ -17,21 +17,36 @@ replace github.com/chzyer/readline => github.com/kylecarbs/readline v0.0.0-20220
|
||||
// opencensus-go leaks a goroutine by default.
|
||||
replace go.opencensus.io => github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b
|
||||
|
||||
// These are to allow embedding the cloudflared quick-tunnel CLI.
|
||||
// Required until https://github.com/cloudflare/cloudflared/pull/597 is merged.
|
||||
replace github.com/cloudflare/cloudflared => github.com/kylecarbs/cloudflared v0.0.0-20220311054120-ea109c6bf7be
|
||||
|
||||
replace github.com/urfave/cli/v2 => github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d
|
||||
|
||||
replace github.com/rivo/tview => github.com/kylecarbs/tview v0.0.0-20220309202238-8464256e10a1
|
||||
|
||||
require (
|
||||
cdr.dev/slog v1.4.1
|
||||
cloud.google.com/go/compute v1.5.0
|
||||
github.com/briandowns/spinner v1.18.1
|
||||
github.com/charmbracelet/bubbles v0.10.3
|
||||
github.com/charmbracelet/bubbletea v0.20.0
|
||||
github.com/charmbracelet/charm v0.10.3
|
||||
github.com/charmbracelet/lipgloss v0.5.0
|
||||
github.com/cloudflare/cloudflared v0.0.0-20220308214351-5352b3cf0489
|
||||
github.com/coder/retry v1.3.0
|
||||
github.com/creack/pty v1.1.17
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/gliderlabs/ssh v0.3.3
|
||||
github.com/go-chi/chi/v5 v5.0.7
|
||||
github.com/go-chi/render v1.0.1
|
||||
github.com/go-playground/validator/v10 v10.10.0
|
||||
github.com/go-playground/validator/v10 v10.10.1
|
||||
github.com/gohugoio/hugo v0.94.0
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/golang-migrate/migrate/v4 v4.15.1
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/hashicorp/go-version v1.4.0
|
||||
github.com/hashicorp/hc-install v0.3.1
|
||||
github.com/hashicorp/terraform-config-inspect v0.0.0-20211115214459-90acf1ca460f
|
||||
github.com/hashicorp/terraform-exec v0.15.0
|
||||
github.com/hashicorp/terraform-json v0.13.0
|
||||
@@ -42,27 +57,29 @@ require (
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/mattn/go-isatty v0.0.14
|
||||
github.com/mitchellh/mapstructure v1.4.3
|
||||
github.com/moby/moby v20.10.12+incompatible
|
||||
github.com/moby/moby v20.10.13+incompatible
|
||||
github.com/ory/dockertest/v3 v3.8.1
|
||||
github.com/pion/datachannel v1.5.2
|
||||
github.com/pion/logging v0.2.2
|
||||
github.com/pion/transport v0.13.0
|
||||
github.com/pion/webrtc/v3 v3.1.24
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||
github.com/powersj/whatsthis v1.3.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.17
|
||||
github.com/spf13/cobra v1.3.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/rs/zerolog v1.26.1
|
||||
github.com/spf13/cobra v1.4.0
|
||||
github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942
|
||||
github.com/tabbed/pqtype v0.1.1
|
||||
github.com/unrolled/secure v1.10.0
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
github.com/xlab/treeprint v1.1.0
|
||||
go.uber.org/atomic v1.9.0
|
||||
go.uber.org/goleak v1.1.12
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158
|
||||
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
|
||||
google.golang.org/api v0.70.0
|
||||
google.golang.org/api v0.71.0
|
||||
google.golang.org/protobuf v1.27.1
|
||||
nhooyr.io/websocket v1.8.7
|
||||
storj.io/drpc v0.0.29
|
||||
@@ -70,49 +87,101 @@ require (
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/BurntSushi/toml v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
github.com/apparentlymart/go-cidr v1.1.0 // indirect
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
||||
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cheekybits/genny v1.0.0 // indirect
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||
github.com/clbanning/mxj/v2 v2.5.5 // indirect
|
||||
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93 // indirect
|
||||
github.com/cloudflare/golibs v0.0.0-20210909181612-21743d7dd02a // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/containerd/continuity v0.2.2 // indirect
|
||||
github.com/coredns/caddy v1.1.1 // indirect
|
||||
github.com/coredns/coredns v1.9.0 // indirect
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dhui/dktest v0.3.9 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/docker/cli v20.10.12+incompatible // indirect
|
||||
github.com/docker/cli v20.10.13+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.0+incompatible // indirect
|
||||
github.com/docker/docker v20.10.12+incompatible // indirect
|
||||
github.com/docker/docker v20.10.13+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434 // indirect
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/gdamore/tcell v1.4.0 // indirect
|
||||
github.com/getsentry/raven-go v0.2.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.1.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.7 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.11.1 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect
|
||||
github.com/klauspost/compress v1.14.3 // indirect
|
||||
github.com/klauspost/compress v1.15.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/lucas-clemente/quic-go v0.25.1-0.20220307142123-ad1cb27c1b64 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/lunixbochs/vtclean v1.0.0 // indirect
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.4 // indirect
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.0 // indirect
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/miekg/dns v1.1.46 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
|
||||
github.com/niklasfasching/go-org v1.6.2 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/opencontainers/runc v1.1.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.0-beta.6 // indirect
|
||||
github.com/pion/dtls/v2 v2.1.3 // indirect
|
||||
github.com/pion/ice/v2 v2.2.1 // indirect
|
||||
github.com/pion/interceptor v0.1.7 // indirect
|
||||
github.com/pion/ice/v2 v2.2.2 // indirect
|
||||
github.com/pion/interceptor v0.1.9 // indirect
|
||||
github.com/pion/mdns v0.0.5 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.9 // indirect
|
||||
@@ -123,9 +192,20 @@ require (
|
||||
github.com/pion/stun v0.3.5 // indirect
|
||||
github.com/pion/turn/v2 v2.0.8 // indirect
|
||||
github.com/pion/udp v0.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pquerna/cachecontrol v0.1.0 // indirect
|
||||
github.com/prometheus/client_golang v1.12.1 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/rivo/tview v0.0.0-20200712113419-c65badfc3d92 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/spf13/afero v1.8.1 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
@@ -133,12 +213,20 @@ require (
|
||||
github.com/zclconf/go-cty v1.10.0 // indirect
|
||||
github.com/zeebo/errs v1.2.2 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
|
||||
golang.org/x/mod v0.5.1 // indirect
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/tools v0.1.9 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect
|
||||
google.golang.org/grpc v1.44.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6 // indirect
|
||||
google.golang.org/grpc v1.45.0 // indirect
|
||||
gopkg.in/coreos/go-oidc.v2 v2.2.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
zombiezen.com/go/capnproto2 v2.18.2+incompatible // indirect
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
"github.com/coder/coder/provisionersdk/proto"
|
||||
)
|
||||
|
||||
@@ -81,6 +82,9 @@ func (t *terraform) runTerraformPlan(ctx context.Context, terraform *tfexec.Terr
|
||||
parts := strings.SplitN(envEntry, "=", 2)
|
||||
env[parts[0]] = parts[1]
|
||||
}
|
||||
for key, value := range provisionersdk.AgentScriptEnv() {
|
||||
env[key] = value
|
||||
}
|
||||
env["CODER_URL"] = request.Metadata.CoderUrl
|
||||
env["CODER_WORKSPACE_TRANSITION"] = strings.ToLower(request.Metadata.WorkspaceTransition.String())
|
||||
planfilePath := filepath.Join(request.Directory, "terraform.tfplan")
|
||||
@@ -288,6 +292,9 @@ func (t *terraform) runTerraformApply(ctx context.Context, terraform *tfexec.Ter
|
||||
"CODER_URL="+request.Metadata.CoderUrl,
|
||||
"CODER_WORKSPACE_TRANSITION="+strings.ToLower(request.Metadata.WorkspaceTransition.String()),
|
||||
)
|
||||
for key, value := range provisionersdk.AgentScriptEnv() {
|
||||
env = append(env, key+"="+value)
|
||||
}
|
||||
vars := []string{}
|
||||
for _, param := range request.ParameterValues {
|
||||
switch param.DestinationScheme {
|
||||
@@ -439,14 +446,27 @@ func (t *terraform) runTerraformApply(ctx context.Context, terraform *tfexec.Ter
|
||||
break
|
||||
}
|
||||
}
|
||||
// Associate resources where the agent depends on it.
|
||||
for agentKey, dependsOn := range agentDepends {
|
||||
for _, depend := range dependsOn {
|
||||
if depend != strings.Join([]string{resource.Type, resource.Name}, ".") {
|
||||
continue
|
||||
if agent == nil {
|
||||
// Associate resources where the agent depends on it.
|
||||
for agentKey, dependsOn := range agentDepends {
|
||||
for _, depend := range dependsOn {
|
||||
if depend != strings.Join([]string{resource.Type, resource.Name}, ".") {
|
||||
continue
|
||||
}
|
||||
agent = agents[agentKey]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if agent != nil {
|
||||
if agent.GetGoogleInstanceIdentity() != nil {
|
||||
// Make sure the instance has an instance ID!
|
||||
_, exists := resource.AttributeValues["instance_id"]
|
||||
if !exists {
|
||||
// This was a mistake!
|
||||
agent = nil
|
||||
}
|
||||
agent = agents[agentKey]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -201,13 +201,6 @@ provider "coder" {
|
||||
Resources: []*proto.Resource{{
|
||||
Name: "A",
|
||||
Type: "null_resource",
|
||||
Agent: &proto.Agent{
|
||||
Auth: &proto.Agent_GoogleInstanceIdentity{
|
||||
GoogleInstanceIdentity: &proto.GoogleInstanceIdentityAuth{
|
||||
InstanceId: "an-instance",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -11,6 +11,9 @@ import (
|
||||
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
"github.com/coder/coder/provisionersdk/proto"
|
||||
|
||||
"github.com/hashicorp/hc-install/product"
|
||||
"github.com/hashicorp/hc-install/releases"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -40,9 +43,19 @@ func Serve(ctx context.Context, options *ServeOptions) error {
|
||||
if options.BinaryPath == "" {
|
||||
binaryPath, err := exec.LookPath("terraform")
|
||||
if err != nil {
|
||||
return xerrors.Errorf("terraform binary not found: %w", err)
|
||||
installer := &releases.ExactVersion{
|
||||
Product: product.Terraform,
|
||||
Version: version.Must(version.NewVersion("1.1.7")),
|
||||
}
|
||||
|
||||
execPath, err := installer.Install(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("install terraform: %w", err)
|
||||
}
|
||||
options.BinaryPath = execPath
|
||||
} else {
|
||||
options.BinaryPath = binaryPath
|
||||
}
|
||||
options.BinaryPath = binaryPath
|
||||
}
|
||||
shutdownCtx, shutdownCancel := context.WithCancel(ctx)
|
||||
return provisionersdk.Serve(ctx, &terraform{
|
||||
|
||||
@@ -10,10 +10,9 @@ var (
|
||||
"windows": {
|
||||
"amd64": `
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
$ErrorActionPreference = "Stop"
|
||||
Invoke-WebRequest -Uri ${ACCESS_URL}/bin/coder-windows-amd64 -OutFile $env:TEMP\coder.exe
|
||||
Invoke-WebRequest -Uri ${ACCESS_URL}bin/coder-windows-amd64.exe -OutFile $env:TEMP\coder.exe
|
||||
$env:CODER_URL = "${ACCESS_URL}"
|
||||
Start-Process -FilePath $env:TEMP\coder.exe workspaces agent
|
||||
Start-Process -FilePath $env:TEMP\coder.exe -ArgumentList "workspaces","agent" -PassThru
|
||||
`,
|
||||
},
|
||||
"linux": {
|
||||
@@ -21,10 +20,10 @@ Start-Process -FilePath $env:TEMP\coder.exe workspaces agent
|
||||
#!/usr/bin/env sh
|
||||
set -eu pipefail
|
||||
BINARY_LOCATION=$(mktemp -d)/coder
|
||||
curl -fsSL ${ACCESS_URL}/bin/coder-linux-amd64 -o $BINARY_LOCATION
|
||||
curl -fsSL ${ACCESS_URL}bin/coder-linux-amd64 -o $BINARY_LOCATION
|
||||
chmod +x $BINARY_LOCATION
|
||||
export CODER_URL="${ACCESS_URL}"
|
||||
exec $BINARY_LOCATION agent
|
||||
exec $BINARY_LOCATION workspaces agent
|
||||
`,
|
||||
},
|
||||
"darwin": {
|
||||
@@ -32,10 +31,10 @@ exec $BINARY_LOCATION agent
|
||||
#!/usr/bin/env sh
|
||||
set -eu pipefail
|
||||
BINARY_LOCATION=$(mktemp -d)/coder
|
||||
curl -fsSL ${ACCESS_URL}/bin/coder-darwin-amd64 -o $BINARY_LOCATION
|
||||
curl -fsSL ${ACCESS_URL}bin/coder-darwin-amd64 -o $BINARY_LOCATION
|
||||
chmod +x $BINARY_LOCATION
|
||||
export CODER_URL="${ACCESS_URL}"
|
||||
exec $BINARY_LOCATION agent
|
||||
exec $BINARY_LOCATION workspaces agent
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -44,12 +44,12 @@ func TestAgentScript(t *testing.T) {
|
||||
t.Skip("Agent not supported...")
|
||||
return
|
||||
}
|
||||
script = strings.ReplaceAll(script, "${ACCESS_URL}", srvURL.String())
|
||||
script = strings.ReplaceAll(script, "${ACCESS_URL}", srvURL.String()+"/")
|
||||
output, err := exec.Command("sh", "-c", script).CombinedOutput()
|
||||
t.Log(string(output))
|
||||
require.NoError(t, err)
|
||||
// Because we use the "echo" binary, we should expect the arguments provided
|
||||
// as the response to executing our script.
|
||||
require.Equal(t, "agent", strings.TrimSpace(string(output)))
|
||||
require.Equal(t, "workspaces agent", strings.TrimSpace(string(output)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package provisionersdk
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Tar archives a directory.
|
||||
func Tar(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
|
||||
}
|
||||
if strings.HasPrefix(rel, ".") {
|
||||
// Don't archive hidden files!
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package provisionersdk_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
)
|
||||
|
||||
func TestTar(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
file, err := ioutil.TempFile(dir, "")
|
||||
require.NoError(t, err)
|
||||
_ = file.Close()
|
||||
_, err = provisionersdk.Tar(dir)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
+6
-3
@@ -14,7 +14,7 @@ type PTY interface {
|
||||
// uses the output stream for writing.
|
||||
//
|
||||
// The same stream could be read to validate output.
|
||||
Output() io.ReadWriter
|
||||
Output() ReadWriter
|
||||
|
||||
// Input handles TTY input.
|
||||
//
|
||||
@@ -22,7 +22,7 @@ type PTY interface {
|
||||
// uses the PTY input for reading.
|
||||
//
|
||||
// The same stream would be used to provide user input: pty.Input().Write(...)
|
||||
Input() io.ReadWriter
|
||||
Input() ReadWriter
|
||||
|
||||
// Resize sets the size of the PTY.
|
||||
Resize(cols uint16, rows uint16) error
|
||||
@@ -33,7 +33,10 @@ func New() (PTY, error) {
|
||||
return newPty()
|
||||
}
|
||||
|
||||
type readWriter struct {
|
||||
// ReadWriter implements io.ReadWriter, but is intentionally avoids
|
||||
// using the interface to allow for direct access to the reader or
|
||||
// writer. This is to enable a caller to grab file descriptors.
|
||||
type ReadWriter struct {
|
||||
io.Reader
|
||||
io.Writer
|
||||
}
|
||||
|
||||
+4
-5
@@ -4,7 +4,6 @@
|
||||
package pty
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
@@ -28,15 +27,15 @@ type otherPty struct {
|
||||
pty, tty *os.File
|
||||
}
|
||||
|
||||
func (p *otherPty) Input() io.ReadWriter {
|
||||
return readWriter{
|
||||
func (p *otherPty) Input() ReadWriter {
|
||||
return ReadWriter{
|
||||
Reader: p.tty,
|
||||
Writer: p.pty,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *otherPty) Output() io.ReadWriter {
|
||||
return readWriter{
|
||||
func (p *otherPty) Output() ReadWriter {
|
||||
return ReadWriter{
|
||||
Reader: p.pty,
|
||||
Writer: p.tty,
|
||||
}
|
||||
|
||||
+4
-5
@@ -4,7 +4,6 @@
|
||||
package pty
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"unsafe"
|
||||
@@ -67,15 +66,15 @@ type ptyWindows struct {
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (p *ptyWindows) Output() io.ReadWriter {
|
||||
return readWriter{
|
||||
func (p *ptyWindows) Output() ReadWriter {
|
||||
return ReadWriter{
|
||||
Reader: p.outputRead,
|
||||
Writer: p.outputWrite,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ptyWindows) Input() io.ReadWriter {
|
||||
return readWriter{
|
||||
func (p *ptyWindows) Input() ReadWriter {
|
||||
return ReadWriter{
|
||||
Reader: p.inputRead,
|
||||
Writer: p.inputWrite,
|
||||
}
|
||||
|
||||
+11
-3
@@ -3,7 +3,6 @@ package ptytest
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -11,6 +10,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -61,6 +61,7 @@ func create(t *testing.T, ptty pty.PTY) *PTY {
|
||||
|
||||
outputWriter: writer,
|
||||
runeReader: bufio.NewReaderSize(ptty.Output(), utf8.UTFMax),
|
||||
runeWriter: bufio.NewWriterSize(ptty.Input(), utf8.UTFMax),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +71,7 @@ type PTY struct {
|
||||
|
||||
outputWriter io.Writer
|
||||
runeReader *bufio.Reader
|
||||
runeWriter *bufio.Writer
|
||||
}
|
||||
|
||||
func (p *PTY) ExpectMatch(str string) string {
|
||||
@@ -93,10 +95,16 @@ func (p *PTY) ExpectMatch(str string) string {
|
||||
}
|
||||
|
||||
func (p *PTY) WriteLine(str string) {
|
||||
newline := "\n"
|
||||
_, err := p.Input().Write([]byte(str))
|
||||
require.NoError(p.t, err)
|
||||
|
||||
// This is jank. Bubbletea requires line returns to be on
|
||||
// a separate read, but this is an inherent race.
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
newline := "\r"
|
||||
if runtime.GOOS == "windows" {
|
||||
newline = "\r\n"
|
||||
}
|
||||
_, err := fmt.Fprintf(p.PTY.Input(), "%s%s", str, newline)
|
||||
_, err = p.Input().Write([]byte(newline))
|
||||
require.NoError(p.t, err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"ID": "gcp-linux",
|
||||
"Name": "Develop in Linux on Google Cloud",
|
||||
"Description": "Get started with Linux development on Google Cloud.",
|
||||
"schema": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"job_id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": "gcp_credentials",
|
||||
"description": "",
|
||||
"default_source_scheme": "none",
|
||||
"default_source_value": "",
|
||||
"allow_override_source": false,
|
||||
"default_destination_scheme": "provisioner_variable",
|
||||
"allow_override_destination": false,
|
||||
"default_refresh": "",
|
||||
"redisplay_value": false,
|
||||
"validation_error": "",
|
||||
"validation_condition": "",
|
||||
"validation_type_system": "none",
|
||||
"validation_value_type": ""
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"job_id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": "gcp_project",
|
||||
"description": "The Google Cloud project to manage resources in.",
|
||||
"default_source_scheme": "none",
|
||||
"default_source_value": "",
|
||||
"allow_override_source": false,
|
||||
"default_destination_scheme": "provisioner_variable",
|
||||
"allow_override_destination": false,
|
||||
"default_refresh": "",
|
||||
"redisplay_value": true,
|
||||
"validation_error": "",
|
||||
"validation_condition": "",
|
||||
"validation_type_system": "none",
|
||||
"validation_value_type": ""
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"job_id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": "gcp_region",
|
||||
"description": "",
|
||||
"default_source_scheme": "data",
|
||||
"default_source_value": "\"us-central1\"",
|
||||
"allow_override_source": false,
|
||||
"default_destination_scheme": "provisioner_variable",
|
||||
"allow_override_destination": false,
|
||||
"default_refresh": "",
|
||||
"redisplay_value": true,
|
||||
"validation_error": "",
|
||||
"validation_condition": "",
|
||||
"validation_type_system": "none",
|
||||
"validation_value_type": ""
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"job_id": "00000000-0000-0000-0000-000000000000",
|
||||
"workspace_transition": "start",
|
||||
"type": "google_compute_instance",
|
||||
"name": "dev",
|
||||
"agent": {
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z",
|
||||
"resource_id": "00000000-0000-0000-0000-000000000000",
|
||||
"environment_variables": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"job_id": "00000000-0000-0000-0000-000000000000",
|
||||
"workspace_transition": "start",
|
||||
"type": "random_string",
|
||||
"name": "random"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
name: Develop in Linux on Google Cloud
|
||||
description: Get started with Linux development on Google Cloud.
|
||||
tags: [cloud, google]
|
||||
---
|
||||
@@ -0,0 +1,73 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "gcp_credentials" {
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "gcp_project" {
|
||||
description = "The Google Cloud project to manage resources in."
|
||||
}
|
||||
|
||||
variable "gcp_region" {
|
||||
default = "us-central1"
|
||||
}
|
||||
|
||||
provider "google" {
|
||||
project = var.gcp_project
|
||||
region = var.gcp_region
|
||||
credentials = var.gcp_credentials
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {
|
||||
}
|
||||
|
||||
data "coder_agent_script" "dev" {
|
||||
arch = "amd64"
|
||||
os = "linux"
|
||||
}
|
||||
|
||||
data "google_compute_default_service_account" "default" {
|
||||
}
|
||||
|
||||
resource "random_string" "random" {
|
||||
count = data.coder_workspace.me.transition == "start" ? 1 : 0
|
||||
length = 8
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "google_compute_instance" "dev" {
|
||||
zone = "us-central1-a"
|
||||
count = data.coder_workspace.me.transition == "start" ? 1 : 0
|
||||
name = "coder-${lower(random_string.random[0].result)}"
|
||||
machine_type = "e2-medium"
|
||||
network_interface {
|
||||
network = "default"
|
||||
access_config {
|
||||
// Ephemeral public IP
|
||||
}
|
||||
}
|
||||
boot_disk {
|
||||
initialize_params {
|
||||
image = "debian-cloud/debian-9"
|
||||
}
|
||||
}
|
||||
service_account {
|
||||
email = data.google_compute_default_service_account.default.email
|
||||
scopes = ["cloud-platform"]
|
||||
}
|
||||
metadata_startup_script = data.coder_agent_script.dev.value
|
||||
}
|
||||
|
||||
resource "coder_agent" "dev" {
|
||||
count = length(google_compute_instance.dev)
|
||||
auth {
|
||||
type = "google-instance-identity"
|
||||
instance_id = google_compute_instance.dev[0].instance_id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"ID": "gcp-windows",
|
||||
"Name": "Develop in Windows on Google Cloud",
|
||||
"Description": "Get started with Windows development on Google Cloud.",
|
||||
"schema": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"job_id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": "gcp_credentials",
|
||||
"description": "",
|
||||
"default_source_scheme": "none",
|
||||
"default_source_value": "",
|
||||
"allow_override_source": false,
|
||||
"default_destination_scheme": "provisioner_variable",
|
||||
"allow_override_destination": false,
|
||||
"default_refresh": "",
|
||||
"redisplay_value": false,
|
||||
"validation_error": "",
|
||||
"validation_condition": "",
|
||||
"validation_type_system": "none",
|
||||
"validation_value_type": ""
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"job_id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": "gcp_project",
|
||||
"description": "The Google Cloud project to manage resources in.",
|
||||
"default_source_scheme": "none",
|
||||
"default_source_value": "",
|
||||
"allow_override_source": false,
|
||||
"default_destination_scheme": "provisioner_variable",
|
||||
"allow_override_destination": false,
|
||||
"default_refresh": "",
|
||||
"redisplay_value": true,
|
||||
"validation_error": "",
|
||||
"validation_condition": "",
|
||||
"validation_type_system": "none",
|
||||
"validation_value_type": ""
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"job_id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": "gcp_region",
|
||||
"description": "",
|
||||
"default_source_scheme": "data",
|
||||
"default_source_value": "\"us-central1\"",
|
||||
"allow_override_source": false,
|
||||
"default_destination_scheme": "provisioner_variable",
|
||||
"allow_override_destination": false,
|
||||
"default_refresh": "",
|
||||
"redisplay_value": true,
|
||||
"validation_error": "",
|
||||
"validation_condition": "",
|
||||
"validation_type_system": "none",
|
||||
"validation_value_type": ""
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"job_id": "00000000-0000-0000-0000-000000000000",
|
||||
"workspace_transition": "start",
|
||||
"type": "google_compute_instance",
|
||||
"name": "dev",
|
||||
"agent": {
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"updated_at": "0001-01-01T00:00:00Z",
|
||||
"resource_id": "00000000-0000-0000-0000-000000000000",
|
||||
"environment_variables": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"job_id": "00000000-0000-0000-0000-000000000000",
|
||||
"workspace_transition": "start",
|
||||
"type": "random_string",
|
||||
"name": "random"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
name: Develop in Windows on Google Cloud
|
||||
description: Get started with Windows development on Google Cloud.
|
||||
tags: [cloud, google]
|
||||
---
|
||||
@@ -0,0 +1,76 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "gcp_credentials" {
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "gcp_project" {
|
||||
description = "The Google Cloud project to manage resources in."
|
||||
}
|
||||
|
||||
variable "gcp_region" {
|
||||
default = "us-central1"
|
||||
}
|
||||
|
||||
provider "google" {
|
||||
project = var.gcp_project
|
||||
region = var.gcp_region
|
||||
credentials = var.gcp_credentials
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {
|
||||
}
|
||||
|
||||
data "coder_agent_script" "dev" {
|
||||
arch = "amd64"
|
||||
os = "windows"
|
||||
}
|
||||
|
||||
data "google_compute_default_service_account" "default" {
|
||||
}
|
||||
|
||||
resource "random_string" "random" {
|
||||
count = data.coder_workspace.me.transition == "start" ? 1 : 0
|
||||
length = 8
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "google_compute_instance" "dev" {
|
||||
zone = "us-central1-a"
|
||||
count = data.coder_workspace.me.transition == "start" ? 1 : 0
|
||||
name = "coder-${lower(random_string.random[0].result)}"
|
||||
machine_type = "e2-medium"
|
||||
network_interface {
|
||||
network = "default"
|
||||
access_config {
|
||||
// Ephemeral public IP
|
||||
}
|
||||
}
|
||||
boot_disk {
|
||||
initialize_params {
|
||||
image = "projects/windows-cloud/global/images/windows-server-2022-dc-core-v20220215"
|
||||
}
|
||||
}
|
||||
service_account {
|
||||
email = data.google_compute_default_service_account.default.email
|
||||
scopes = ["cloud-platform"]
|
||||
}
|
||||
metadata = {
|
||||
windows-startup-script-ps1 = data.coder_agent_script.dev.value
|
||||
serial-port-enable = "TRUE"
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_agent" "dev" {
|
||||
count = length(google_compute_instance.dev)
|
||||
auth {
|
||||
type = "google-instance-identity"
|
||||
instance_id = google_compute_instance.dev[0].instance_id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//go:build !slim
|
||||
// +build !slim
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed */*.json
|
||||
//go:embed */*.tf
|
||||
files embed.FS
|
||||
|
||||
list []codersdk.Template = make([]codersdk.Template, 0)
|
||||
archives map[string][]byte = map[string][]byte{}
|
||||
)
|
||||
|
||||
// Parses templates from the embedded archive and inserts them into the map.
|
||||
func init() {
|
||||
dirs, err := files.ReadDir(".")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
// Each one of these is a template!
|
||||
templateData, err := files.ReadFile(path.Join(dir.Name(), dir.Name()+".json"))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("template %q does not contain compiled json: %s", dir.Name(), err))
|
||||
}
|
||||
templateSrc, err := files.ReadFile(path.Join(dir.Name(), dir.Name()+".tf"))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("template %q does not contain terraform source: %s", dir.Name(), err))
|
||||
}
|
||||
|
||||
var template codersdk.Template
|
||||
err = json.Unmarshal(templateData, &template)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("unmarshal template %q: %s", dir.Name(), err))
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
tarWriter := tar.NewWriter(&buffer)
|
||||
err = tarWriter.WriteHeader(&tar.Header{
|
||||
Typeflag: tar.TypeReg,
|
||||
Name: dir.Name() + ".tf",
|
||||
Size: int64(len(templateSrc)),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, err = tarWriter.Write(templateSrc)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = tarWriter.Flush()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
archives[dir.Name()] = buffer.Bytes()
|
||||
list = append(list, template)
|
||||
}
|
||||
}
|
||||
|
||||
// List returns all embedded templates.
|
||||
func List() []codersdk.Template {
|
||||
return list
|
||||
}
|
||||
|
||||
// Archive returns a tar by template ID.
|
||||
func Archive(id string) ([]byte, bool) {
|
||||
data, exists := archives[id]
|
||||
return data, exists
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build slim
|
||||
// +build slim
|
||||
|
||||
package template
|
||||
|
||||
import "github.com/coder/coder/codersdk"
|
||||
|
||||
// List returns all embedded templates.
|
||||
func List() []codersdk.Template {
|
||||
return []codersdk.Template{}
|
||||
}
|
||||
|
||||
// Archive returns a tar by template ID.
|
||||
func Archive(_ string) ([]byte, bool) {
|
||||
return nil, false
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//go:build !slim
|
||||
// +build !slim
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/template"
|
||||
)
|
||||
|
||||
func TestTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
list := template.List()
|
||||
require.Greater(t, len(list), 0)
|
||||
|
||||
_, exists := template.Archive(list[0].ID)
|
||||
require.True(t, exists)
|
||||
}
|
||||
Reference in New Issue
Block a user