Compare commits

...

5 Commits

Author SHA1 Message Date
Kyle Carberry b23a58c8fe It works! 2022-03-11 20:30:12 +00:00
Kyle Carberry 11e1d99625 It all works, but now with tea! 🧋 2022-03-11 03:02:24 +00:00
Kyle Carberry 8cee1fbdba Back to green tests! 2022-03-11 02:29:37 +00:00
Kyle Carberry 688ab17030 Move API structs to codersdk 2022-03-10 19:44:57 +00:00
Kyle Carberry 94e50698e7 Add templates 2022-03-10 18:54:40 +00:00
85 changed files with 3542 additions and 935 deletions
+5
View File
@@ -27,3 +27,8 @@ coverage/
# Build
dist/
site/out/
*.tfstate
*.tfplan
*.lock.hcl
.terraform/
+3 -2
View File
@@ -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
+2 -1
View File
@@ -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",
+5 -1
View File
@@ -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
View File
@@ -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 {
+49
View File
@@ -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
+111
View File
@@ -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()
}
+107
View File
@@ -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
}
+54
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+6 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
},
+9 -8
View File
@@ -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,
})
+2 -2
View File
@@ -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]
+75
View File
@@ -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)
}
}
+353
View File
@@ -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
}
+4
View File
@@ -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)
+17 -17
View File
@@ -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(),
})
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+6 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+3 -20
View File
@@ -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,
-2
View File
@@ -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
View File
@@ -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
+4 -4
View File
@@ -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,
})
+32
View File
@@ -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)
}
+28
View File
@@ -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)
}
+106
View File
@@ -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"`
}
+51
View File
@@ -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
View File
@@ -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
View File
@@ -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,
})
+6 -23
View File
@@ -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,
+4 -5
View File
@@ -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,
})
+3 -12
View File
@@ -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(),
})
}
+17 -4
View File
@@ -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,
})
+17 -45
View File
@@ -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,
+2 -3
View File
@@ -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
View File
@@ -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 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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(&param)
}
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(&parameters)
}
+26 -11
View File
@@ -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
View File
@@ -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(&params)
}
// 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(&params)
}
// 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)
}
+38 -6
View File
@@ -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 {
+47
View File
@@ -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
View File
@@ -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)
}
+25 -9
View File
@@ -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)
}
+16 -8
View File
@@ -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)
}
+52 -6
View File
@@ -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
View File
@@ -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
View File
@@ -250,7 +250,9 @@ SELECT
FROM
workspace_agent
WHERE
auth_token = $1;
auth_token = $1
ORDER BY
created_at DESC;
-- name: GetWorkspaceAgentByInstanceID :one
SELECT
+2
View File
@@ -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) {
+106 -18
View File
@@ -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
)
+761 -51
View File
File diff suppressed because it is too large Load Diff
+27 -7
View File
@@ -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
}
}
-7
View File
@@ -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",
},
},
},
}},
},
},
+15 -2
View File
@@ -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{
+6 -7
View File
@@ -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
`,
},
}
+2 -2
View File
@@ -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)))
})
}
+56
View File
@@ -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
}
+20
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+86
View File
@@ -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"
}
]
}
+5
View File
@@ -0,0 +1,5 @@
---
name: Develop in Linux on Google Cloud
description: Get started with Linux development on Google Cloud.
tags: [cloud, google]
---
+73
View File
@@ -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
}
}
+86
View File
@@ -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"
}
]
}
+5
View File
@@ -0,0 +1,5 @@
---
name: Develop in Windows on Google Cloud
description: Get started with Windows development on Google Cloud.
tags: [cloud, google]
---
+76
View File
@@ -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
}
}
+81
View File
@@ -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
}
+16
View File
@@ -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
}
+21
View File
@@ -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)
}