feat: Refactor API routes to use UUIDs instead of friendly names (#401)

* Add client for agent

* Cleanup code

* Fix linting error

* Rename routes to be simpler

* Rename workspace history to workspace build

* Refactor HTTP middlewares to use UUIDs

* Cleanup routes

* Compiles!

* Fix files and organizations

* Fix querying

* Fix agent lock

* Cleanup database abstraction

* Add parameters

* Fix linting errors

* Fix log race

* Lock on close wait

* Fix log cleanup

* Fix e2e tests

* Fix upstream version of opencensus-go

* Update coderdtest.go

* Fix coverpkg

* Fix codecov ignore
This commit is contained in:
Kyle Carberry 2022-03-07 11:40:54 -06:00 committed by GitHub
parent 330686f60a
commit bf0ae8f573
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
115 changed files with 5853 additions and 4657 deletions

View File

@ -157,6 +157,7 @@ jobs:
GOMAXPROCS: ${{ runner.os == 'Windows' && 1 || 2 }}
run: gotestsum --junitfile="gotests.xml" --packages="./..." --
-covermode=atomic -coverprofile="gotests.coverage"
-coverpkg=./...,github.com/coder/coder/codersdk
-timeout=3m -count=$GOCOUNT -race -short -failfast
- name: Upload DataDog Trace
@ -172,6 +173,7 @@ jobs:
if: runner.os == 'Linux'
run: DB=true gotestsum --junitfile="gotests.xml" --packages="./..." --
-covermode=atomic -coverprofile="gotests.coverage" -timeout=3m
-coverpkg=./...,github.com/coder/coder/codersdk
-count=1 -race -parallel=2 -failfast
- name: Upload DataDog Trace

View File

@ -6,6 +6,9 @@
"go.lintFlags": ["--fast"],
"go.lintOnSave": "package",
"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.coverageDecorator": {
"type": "gutter",
"coveredHighlightColor": "rgba(64,128,128,0.5)",

View File

@ -59,9 +59,9 @@ type Options struct {
Logger slog.Logger
}
type Dialer func(ctx context.Context) (*peerbroker.Listener, error)
type Dialer func(ctx context.Context, options *peer.ConnOptions) (*peerbroker.Listener, error)
func New(dialer Dialer, options *Options) io.Closer {
func New(dialer Dialer, options *peer.ConnOptions) io.Closer {
ctx, cancelFunc := context.WithCancel(context.Background())
server := &server{
clientDialer: dialer,
@ -75,11 +75,12 @@ func New(dialer Dialer, options *Options) io.Closer {
type server struct {
clientDialer Dialer
options *Options
options *peer.ConnOptions
closeCancel context.CancelFunc
closeMutex sync.Mutex
closed chan struct{}
connCloseWait sync.WaitGroup
closeCancel context.CancelFunc
closeMutex sync.Mutex
closed chan struct{}
sshServer *ssh.Server
}
@ -249,7 +250,7 @@ func (s *server) run(ctx context.Context) {
// An exponential back-off occurs when the connection is failing to dial.
// This is to prevent server spam in case of a coderd outage.
for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(ctx); {
peerListener, err = s.clientDialer(ctx)
peerListener, err = s.clientDialer(ctx, s.options)
if err != nil {
if errors.Is(err, context.Canceled) {
return
@ -279,11 +280,18 @@ func (s *server) run(ctx context.Context) {
s.run(ctx)
return
}
s.closeMutex.Lock()
s.connCloseWait.Add(1)
s.closeMutex.Unlock()
go s.handlePeerConn(ctx, conn)
}
}
func (s *server) handlePeerConn(ctx context.Context, conn *peer.Conn) {
go func() {
<-conn.Closed()
s.connCloseWait.Done()
}()
for {
channel, err := conn.Accept(ctx)
if err != nil {
@ -325,5 +333,6 @@ func (s *server) Close() error {
close(s.closed)
s.closeCancel()
_ = s.sshServer.Close()
s.connCloseWait.Wait()
return nil
}

View File

@ -94,11 +94,9 @@ func TestAgent(t *testing.T) {
func setup(t *testing.T) proto.DRPCPeerBrokerClient {
client, server := provisionersdk.TransportPipe()
closer := agent.New(func(ctx context.Context) (*peerbroker.Listener, error) {
return peerbroker.Listen(server, nil, &peer.ConnOptions{
Logger: slogtest.Make(t, nil),
})
}, &agent.Options{
closer := agent.New(func(ctx context.Context, opts *peer.ConnOptions) (*peerbroker.Listener, error) {
return peerbroker.Listen(server, nil, opts)
}, &peer.ConnOptions{
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
})
t.Cleanup(func() {

View File

@ -59,7 +59,7 @@ func login() *cobra.Command {
}
client := codersdk.New(serverURL)
hasInitialUser, err := client.HasInitialUser(cmd.Context())
hasInitialUser, err := client.HasFirstUser(cmd.Context())
if err != nil {
return xerrors.Errorf("has initial user: %w", err)
}
@ -119,7 +119,7 @@ func login() *cobra.Command {
return xerrors.Errorf("specify password prompt: %w", err)
}
_, err = client.CreateInitialUser(cmd.Context(), coderd.CreateInitialUserRequest{
_, err = client.CreateFirstUser(cmd.Context(), coderd.CreateFirstUserRequest{
Email: email,
Username: username,
Password: password,

View File

@ -1,13 +1,11 @@
package cli_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/pty/ptytest"
)
@ -56,18 +54,7 @@ func TestLogin(t *testing.T) {
t.Run("ExistingUserValidTokenTTY", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{
Username: "test-user",
Email: "test-user@coder.com",
Organization: "acme-corp",
Password: "password",
})
require.NoError(t, err)
token, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: "test-user@coder.com",
Password: "password",
})
require.NoError(t, err)
coderdtest.CreateFirstUser(t, client)
root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty", "--no-open")
pty := ptytest.New(t)
@ -79,20 +66,14 @@ func TestLogin(t *testing.T) {
}()
pty.ExpectMatch("Paste your token here:")
pty.WriteLine(token.SessionToken)
pty.WriteLine(client.SessionToken)
pty.ExpectMatch("Welcome to Coder")
})
t.Run("ExistingUserInvalidTokenTTY", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{
Username: "test-user",
Email: "test-user@coder.com",
Organization: "acme-corp",
Password: "password",
})
require.NoError(t, err)
coderdtest.CreateFirstUser(t, client)
root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty", "--no-open")
pty := ptytest.New(t)

View File

@ -56,7 +56,7 @@ func projectCreate() *cobra.Command {
Default: filepath.Base(directory),
Label: "What's your project's name?",
Validate: func(s string) error {
project, _ := client.Project(cmd.Context(), organization.Name, s)
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!")
}
@ -71,9 +71,9 @@ func projectCreate() *cobra.Command {
if err != nil {
return err
}
project, err := client.CreateProject(cmd.Context(), organization.Name, coderd.CreateProjectRequest{
Name: name,
VersionImportJobID: job.ID,
project, err := client.CreateProject(cmd.Context(), organization.ID, coderd.CreateProjectRequest{
Name: name,
VersionID: job.ID,
})
if err != nil {
return err
@ -118,7 +118,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.CreateParameterValueRequest) (*coderd.ProvisionerJob, error) {
func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, organization coderd.Organization, provisioner database.ProvisionerType, directory string, parameters ...coderd.CreateParameterRequest) (*coderd.ProjectVersion, error) {
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
spin.Writer = cmd.OutOrStdout()
spin.Suffix = " Uploading current directory..."
@ -133,13 +133,13 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o
if err != nil {
return nil, err
}
resp, err := client.UploadFile(cmd.Context(), codersdk.ContentTypeTar, tarData)
resp, err := client.Upload(cmd.Context(), codersdk.ContentTypeTar, tarData)
if err != nil {
return nil, err
}
before := time.Now()
job, err := client.CreateProjectImportJob(cmd.Context(), organization.Name, coderd.CreateProjectImportJobRequest{
version, err := client.CreateProjectVersion(cmd.Context(), organization.ID, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProvisionerStorageMethodFile,
StorageSource: resp.Hash,
Provisioner: provisioner,
@ -149,7 +149,7 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o
return nil, err
}
spin.Suffix = " Waiting for the import to complete..."
logs, err := client.ProjectImportJobLogsAfter(cmd.Context(), organization.Name, job.ID, before)
logs, err := client.ProjectVersionLogsAfter(cmd.Context(), version.ID, before)
if err != nil {
return nil, err
}
@ -162,22 +162,22 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o
logBuffer = append(logBuffer, log)
}
job, err = client.ProjectImportJob(cmd.Context(), organization.Name, job.ID)
version, err = client.ProjectVersion(cmd.Context(), version.ID)
if err != nil {
return nil, err
}
parameterSchemas, err := client.ProjectImportJobSchemas(cmd.Context(), organization.Name, job.ID)
parameterSchemas, err := client.ProjectVersionSchema(cmd.Context(), version.ID)
if err != nil {
return nil, err
}
parameterValues, err := client.ProjectImportJobParameters(cmd.Context(), organization.Name, job.ID)
parameterValues, err := client.ProjectVersionParameters(cmd.Context(), version.ID)
if err != nil {
return nil, err
}
spin.Stop()
if provisionerd.IsMissingParameterError(job.Error) {
valuesBySchemaID := map[string]coderd.ComputedParameterValue{}
if provisionerd.IsMissingParameterError(version.Job.Error) {
valuesBySchemaID := map[string]coderd.ProjectVersionParameter{}
for _, parameterValue := range parameterValues {
valuesBySchemaID[parameterValue.SchemaID.String()] = parameterValue
}
@ -192,7 +192,7 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o
if err != nil {
return nil, err
}
parameters = append(parameters, coderd.CreateParameterValueRequest{
parameters = append(parameters, coderd.CreateParameterRequest{
Name: parameterSchema.Name,
SourceValue: value,
SourceScheme: database.ParameterSourceSchemeData,
@ -202,21 +202,20 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o
return validateProjectVersionSource(cmd, client, organization, provisioner, directory, parameters...)
}
if job.Status != coderd.ProvisionerJobStatusSucceeded {
if version.Job.Status != coderd.ProvisionerJobSucceeded {
for _, log := range logBuffer {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s %s\n", color.HiGreenString("[tf]"), log.Output)
}
return nil, xerrors.New(job.Error)
return nil, xerrors.New(version.Job.Error)
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Successfully imported project source!\n", color.HiGreenString("✓"))
resources, err := client.ProjectImportJobResources(cmd.Context(), organization.Name, job.ID)
resources, err := client.ProjectVersionResources(cmd.Context(), version.ID)
if err != nil {
return nil, err
}
return &job, displayProjectImportInfo(cmd, parameterSchemas, parameterValues, resources)
return &version, displayProjectImportInfo(cmd, parameterSchemas, parameterValues, resources)
}
func tarDirectory(directory string) ([]byte, error) {

View File

@ -18,7 +18,7 @@ func TestProjectCreate(t *testing.T) {
t.Run("NoParameters", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateInitialUser(t, client)
coderdtest.CreateFirstUser(t, client)
source := clitest.CreateProjectVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
Provision: echo.ProvisionComplete,
@ -54,7 +54,7 @@ func TestProjectCreate(t *testing.T) {
t.Run("Parameter", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateInitialUser(t, client)
coderdtest.CreateFirstUser(t, client)
source := clitest.CreateProjectVersionSource(t, &echo.Responses{
Parse: []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{

View File

@ -23,7 +23,7 @@ func projectList() *cobra.Command {
if err != nil {
return err
}
projects, err := client.Projects(cmd.Context(), organization.Name)
projects, err := client.ProjectsByOrganization(cmd.Context(), organization.ID)
if err != nil {
return err
}

View File

@ -15,7 +15,7 @@ func TestProjectList(t *testing.T) {
t.Run("None", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateInitialUser(t, client)
coderdtest.CreateFirstUser(t, client)
cmd, root := clitest.New(t, "projects", "list")
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
@ -33,12 +33,12 @@ func TestProjectList(t *testing.T) {
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
user := coderdtest.CreateFirstUser(t, client)
daemon := coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
_ = daemon.Close()
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
cmd, root := clitest.New(t, "projects", "list")
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)

View File

@ -40,8 +40,8 @@ func projects() *cobra.Command {
return cmd
}
func displayProjectImportInfo(cmd *cobra.Command, parameterSchemas []coderd.ParameterSchema, parameterValues []coderd.ComputedParameterValue, resources []coderd.ProvisionerJobResource) error {
schemaByID := map[string]coderd.ParameterSchema{}
func displayProjectImportInfo(cmd *cobra.Command, parameterSchemas []coderd.ProjectVersionParameterSchema, parameterValues []coderd.ProjectVersionParameter, resources []coderd.WorkspaceResource) error {
schemaByID := map[string]coderd.ProjectVersionParameterSchema{}
for _, schema := range parameterSchemas {
schemaByID[schema.ID.String()] = schema
}

View File

@ -108,7 +108,7 @@ 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) {
orgs, err := client.UserOrganizations(cmd.Context(), "me")
orgs, err := client.OrganizationsByUser(cmd.Context(), "me")
if err != nil {
return coderd.Organization{}, nil
}

57
cli/workspaceagent.go Normal file
View File

@ -0,0 +1,57 @@
package cli
import (
"net/url"
"os"
"github.com/powersj/whatsthis/pkg/cloud"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/agent"
"github.com/coder/coder/codersdk"
)
func workspaceAgent() *cobra.Command {
return &cobra.Command{
Use: "agent",
// This command isn't useful for users, and seems
// more likely to confuse.
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
coderURLRaw, exists := os.LookupEnv("CODER_URL")
if !exists {
return xerrors.New("CODER_URL must be set")
}
coderURL, err := url.Parse(coderURLRaw)
if err != nil {
return xerrors.Errorf("parse %q: %w", coderURLRaw, err)
}
client := codersdk.New(coderURL)
sessionToken, exists := os.LookupEnv("CODER_TOKEN")
if !exists {
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("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)
<-cmd.Context().Done()
return closer.Close()
},
}
}

View File

@ -39,7 +39,7 @@ func workspaceCreate() *cobra.Command {
if s == "" {
return xerrors.Errorf("You must provide a name!")
}
workspace, _ := client.Workspace(cmd.Context(), "", s)
workspace, _ := client.WorkspaceByName(cmd.Context(), "", s)
if workspace.ID.String() != uuid.Nil.String() {
return xerrors.New("A workspace already exists with that name!")
}
@ -56,23 +56,23 @@ func workspaceCreate() *cobra.Command {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Previewing project create...\n", caret)
project, err := client.Project(cmd.Context(), organization.Name, args[0])
project, err := client.ProjectByName(cmd.Context(), organization.ID, args[0])
if err != nil {
return err
}
projectVersion, err := client.ProjectVersion(cmd.Context(), organization.Name, project.Name, project.ActiveVersionID.String())
projectVersion, err := client.ProjectVersion(cmd.Context(), project.ActiveVersionID)
if err != nil {
return err
}
parameterSchemas, err := client.ProjectImportJobSchemas(cmd.Context(), organization.Name, projectVersion.ImportJobID)
parameterSchemas, err := client.ProjectVersionSchema(cmd.Context(), projectVersion.ID)
if err != nil {
return err
}
parameterValues, err := client.ProjectImportJobParameters(cmd.Context(), organization.Name, projectVersion.ImportJobID)
parameterValues, err := client.ProjectVersionParameters(cmd.Context(), projectVersion.ID)
if err != nil {
return err
}
resources, err := client.ProjectImportJobResources(cmd.Context(), organization.Name, projectVersion.ImportJobID)
resources, err := client.ProjectVersionResources(cmd.Context(), projectVersion.ID)
if err != nil {
return err
}
@ -100,7 +100,7 @@ func workspaceCreate() *cobra.Command {
if err != nil {
return err
}
history, err := client.CreateWorkspaceHistory(cmd.Context(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
version, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: projectVersion.ID,
Transition: database.WorkspaceTransitionStart,
})
@ -108,7 +108,7 @@ func workspaceCreate() *cobra.Command {
return err
}
logs, err := client.WorkspaceProvisionJobLogsAfter(cmd.Context(), organization.Name, history.ProvisionJobID, time.Time{})
logs, err := client.WorkspaceBuildLogsAfter(cmd.Context(), version.ID, time.Time{})
if err != nil {
return err
}

View File

@ -17,9 +17,9 @@ func TestWorkspaceCreate(t *testing.T) {
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
@ -32,8 +32,8 @@ func TestWorkspaceCreate(t *testing.T) {
},
}},
})
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
cmd, root := clitest.New(t, "workspaces", "create", project.Name)
clitest.SetupConfig(t, client, root)

View File

@ -6,6 +6,7 @@ func workspaces() *cobra.Command {
cmd := &cobra.Command{
Use: "workspaces",
}
cmd.AddCommand(workspaceAgent())
cmd.AddCommand(workspaceCreate())
return cmd

View File

@ -26,9 +26,13 @@ ignore:
# This is generated code.
- database/models.go
- database/query.sql.go
# All coderd tests fail if this doesn't work.
- database/databasefake
# These are generated or don't require tests.
- cmd
- database/dump
- database/postgres
- peerbroker/proto
- provisionerd/proto
- provisionersdk/proto
- scripts/datadog-cireport
- rules.go

View File

@ -98,7 +98,7 @@ func newProvisionerDaemon(ctx context.Context, client *codersdk.Client, logger s
if err != nil {
return nil, err
}
return provisionerd.New(client.ProvisionerDaemonClient, &provisionerd.Options{
return provisionerd.New(client.ListenProvisionerDaemon, &provisionerd.Options{
Logger: logger,
PollInterval: 50 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,

View File

@ -41,119 +41,131 @@ func New(options *Options) (http.Handler, func()) {
Message: "👋",
})
})
r.Post("/login", api.postLogin)
r.Post("/logout", api.postLogout)
// Used for setup.
r.Get("/user", api.user)
r.Post("/user", api.postUser)
r.Route("/users", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
)
r.Post("/", api.postUsers)
r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/", api.userByName)
r.Get("/organizations", api.organizationsByUser)
r.Post("/keys", api.postKeyForUser)
})
r.Route("/files", func(r chi.Router) {
r.Use(httpmw.ExtractAPIKey(options.Database, nil))
r.Get("/{hash}", api.fileByHash)
r.Post("/", api.postFile)
})
r.Route("/projects", func(r chi.Router) {
r.Route("/organizations/{organization}", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
httpmw.ExtractOrganizationParam(options.Database),
)
r.Get("/", api.projects)
r.Route("/{organization}", func(r chi.Router) {
r.Use(httpmw.ExtractOrganizationParam(options.Database))
r.Get("/", api.projectsByOrganization)
r.Get("/", api.organization)
r.Get("/provisionerdaemons", api.provisionerDaemonsByOrganization)
r.Post("/projectversions", api.postProjectVersionsByOrganization)
r.Route("/projects", func(r chi.Router) {
r.Post("/", api.postProjectsByOrganization)
r.Route("/{project}", func(r chi.Router) {
r.Use(httpmw.ExtractProjectParam(options.Database))
r.Get("/", api.projectByOrganization)
r.Get("/workspaces", api.workspacesByProject)
r.Route("/parameters", func(r chi.Router) {
r.Get("/", api.parametersByProject)
r.Post("/", api.postParametersByProject)
r.Get("/", api.projectsByOrganization)
r.Get("/{projectname}", api.projectByOrganizationAndName)
})
})
r.Route("/parameters/{scope}/{id}", func(r chi.Router) {
r.Use(httpmw.ExtractAPIKey(options.Database, nil))
r.Post("/", api.postParameter)
r.Get("/", api.parameters)
r.Route("/{name}", func(r chi.Router) {
r.Delete("/", api.deleteParameter)
})
})
r.Route("/projects/{project}", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
httpmw.ExtractProjectParam(options.Database),
httpmw.ExtractOrganizationParam(options.Database),
)
r.Get("/", api.project)
r.Route("/versions", func(r chi.Router) {
r.Get("/", api.projectVersionsByProject)
r.Patch("/versions", nil)
r.Get("/{projectversionname}", api.projectVersionByName)
})
})
r.Route("/projectversions/{projectversion}", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
httpmw.ExtractProjectVersionParam(options.Database),
httpmw.ExtractOrganizationParam(options.Database),
)
r.Get("/", api.projectVersion)
r.Get("/schema", api.projectVersionSchema)
r.Get("/parameters", api.projectVersionParameters)
r.Get("/resources", api.projectVersionResources)
r.Get("/logs", api.projectVersionLogs)
})
r.Route("/provisionerdaemons", func(r chi.Router) {
r.Route("/me", func(r chi.Router) {
r.Get("/listen", api.provisionerDaemonsListen)
})
})
r.Route("/users", func(r chi.Router) {
r.Get("/first", api.firstUser)
r.Post("/first", api.postFirstUser)
r.Post("/login", api.postLogin)
r.Post("/logout", api.postLogout)
r.Group(func(r chi.Router) {
r.Use(httpmw.ExtractAPIKey(options.Database, nil))
r.Post("/", api.postUsers)
r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/", api.userByName)
r.Get("/organizations", api.organizationsByUser)
r.Post("/organizations", api.postOrganizationsByUser)
r.Post("/keys", api.postAPIKey)
r.Route("/organizations", func(r chi.Router) {
r.Post("/", api.postOrganizationsByUser)
r.Get("/", api.organizationsByUser)
r.Get("/{organizationname}", api.organizationByUserAndName)
})
r.Route("/versions", func(r chi.Router) {
r.Get("/", api.projectVersionsByOrganization)
r.Post("/", api.postProjectVersionByOrganization)
r.Route("/{projectversion}", func(r chi.Router) {
r.Use(httpmw.ExtractProjectVersionParam(api.Database))
r.Get("/", api.projectVersionByOrganizationAndName)
})
r.Route("/workspaces", func(r chi.Router) {
r.Post("/", api.postWorkspacesByUser)
r.Get("/", api.workspacesByUser)
r.Get("/{workspacename}", api.workspaceByUserAndName)
})
})
})
})
// Listing operations specific to resources should go under
// their respective routes. eg. /orgs/<name>/workspaces
r.Route("/workspaces", func(r chi.Router) {
r.Use(httpmw.ExtractAPIKey(options.Database, nil))
r.Get("/", api.workspaces)
r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database))
r.Post("/", api.postWorkspaceByUser)
r.Route("/{workspace}", func(r chi.Router) {
r.Use(httpmw.ExtractWorkspaceParam(options.Database))
r.Get("/", api.workspaceByUser)
r.Route("/version", func(r chi.Router) {
r.Post("/", api.postWorkspaceHistoryByUser)
r.Get("/", api.workspaceHistoryByUser)
r.Route("/{workspacehistory}", func(r chi.Router) {
r.Use(httpmw.ExtractWorkspaceHistoryParam(options.Database))
r.Get("/", api.workspaceHistoryByName)
})
})
})
r.Route("/workspaceresources", func(r chi.Router) {
r.Route("/auth", func(r chi.Router) {
r.Post("/google-instance-identity", api.postWorkspaceAuthGoogleInstanceIdentity)
})
r.Route("/agent", func(r chi.Router) {
r.Use(httpmw.ExtractWorkspaceAgent(options.Database))
r.Get("/", api.workspaceAgentListen)
})
r.Route("/{workspaceresource}", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
httpmw.ExtractWorkspaceResourceParam(options.Database),
httpmw.ExtractWorkspaceParam(options.Database),
)
r.Get("/", api.workspaceResource)
r.Get("/dial", api.workspaceResourceDial)
})
})
r.Route("/workspaceagent", func(r chi.Router) {
r.Route("/authenticate", func(r chi.Router) {
r.Post("/google-instance-identity", api.postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity)
})
})
r.Route("/upload", func(r chi.Router) {
r.Use(httpmw.ExtractAPIKey(options.Database, nil))
r.Post("/", api.postUpload)
})
r.Route("/projectimport/{organization}", func(r chi.Router) {
r.Route("/workspaces/{workspace}", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
httpmw.ExtractOrganizationParam(options.Database),
httpmw.ExtractWorkspaceParam(options.Database),
)
r.Post("/", api.postProjectImportByOrganization)
r.Route("/{provisionerjob}", func(r chi.Router) {
r.Use(httpmw.ExtractProvisionerJobParam(options.Database))
r.Get("/", api.provisionerJobByID)
r.Get("/schemas", api.projectImportJobSchemasByID)
r.Get("/parameters", api.projectImportJobParametersByID)
r.Get("/resources", api.projectImportJobResourcesByID)
r.Get("/logs", api.provisionerJobLogsByID)
r.Get("/", api.workspace)
r.Route("/builds", func(r chi.Router) {
r.Get("/", api.workspaceBuilds)
r.Post("/", api.postWorkspaceBuilds)
r.Get("/latest", api.workspaceBuildLatest)
r.Get("/{workspacebuildname}", api.workspaceBuildByName)
})
})
r.Route("/workspaceprovision/{organization}", func(r chi.Router) {
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
httpmw.ExtractOrganizationParam(options.Database),
httpmw.ExtractWorkspaceBuildParam(options.Database),
httpmw.ExtractWorkspaceParam(options.Database),
)
r.Route("/{provisionerjob}", func(r chi.Router) {
r.Use(httpmw.ExtractProvisionerJobParam(options.Database))
r.Get("/", api.provisionerJobByID)
r.Get("/logs", api.provisionerJobLogsByID)
})
})
r.Route("/provisioners/daemons", func(r chi.Router) {
r.Get("/", api.provisionerDaemons)
r.Get("/serve", api.provisionerDaemonsServe)
r.Get("/", api.workspaceBuild)
r.Get("/logs", api.workspaceBuildLogs)
r.Get("/resources", api.workspaceBuildResources)
})
})
r.NotFound(site.Handler(options.Logger).ServeHTTP)

View File

@ -15,7 +15,6 @@ import (
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"github.com/stretchr/testify/require"
"go.opencensus.io/stats/view"
"google.golang.org/api/idtoken"
"google.golang.org/api/option"
@ -39,13 +38,6 @@ type Options struct {
// New constructs an in-memory coderd instance and returns
// the connected client.
func New(t *testing.T, options *Options) *codersdk.Client {
// Stops the opencensus.io worker from leaking a goroutine.
// The worker isn't used anyways, and is an indirect dependency
// of the Google Cloud SDK.
t.Cleanup(func() {
view.Stop()
})
if options == nil {
options = &Options{}
}
@ -125,7 +117,7 @@ func NewProvisionerDaemon(t *testing.T, client *codersdk.Client) io.Closer {
require.NoError(t, err)
}()
closer := provisionerd.New(client.ProvisionerDaemonClient, &provisionerd.Options{
closer := provisionerd.New(client.ListenProvisionerDaemon, &provisionerd.Options{
Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug),
PollInterval: 50 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
@ -140,16 +132,16 @@ func NewProvisionerDaemon(t *testing.T, client *codersdk.Client) io.Closer {
return closer
}
// CreateInitialUser creates a user with preset credentials and authenticates
// CreateFirstUser creates a user with preset credentials and authenticates
// with the passed in codersdk client.
func CreateInitialUser(t *testing.T, client *codersdk.Client) coderd.CreateInitialUserRequest {
req := coderd.CreateInitialUserRequest{
func CreateFirstUser(t *testing.T, client *codersdk.Client) coderd.CreateFirstUserResponse {
req := coderd.CreateFirstUserRequest{
Email: "testuser@coder.com",
Username: "testuser",
Password: "testpass",
Organization: "testorg",
}
_, err := client.CreateInitialUser(context.Background(), req)
resp, err := client.CreateFirstUser(context.Background(), req)
require.NoError(t, err)
login, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
@ -158,59 +150,101 @@ func CreateInitialUser(t *testing.T, client *codersdk.Client) coderd.CreateIniti
})
require.NoError(t, err)
client.SessionToken = login.SessionToken
return req
return resp
}
// CreateProjectImportJob creates a project import provisioner job
// CreateAnotherUser creates and authenticates a new user.
func CreateAnotherUser(t *testing.T, client *codersdk.Client, organization string) *codersdk.Client {
req := coderd.CreateUserRequest{
Email: namesgenerator.GetRandomName(1) + "@coder.com",
Username: randomUsername(),
Password: "testpass",
OrganizationID: organization,
}
_, err := client.CreateUser(context.Background(), req)
require.NoError(t, err)
login, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: req.Email,
Password: req.Password,
})
require.NoError(t, err)
other := codersdk.New(client.URL)
other.SessionToken = login.SessionToken
return other
}
// CreateProjectVersion creates a project import provisioner job
// with the responses provided. It uses the "echo" provisioner for compatibility
// with testing.
func CreateProjectImportJob(t *testing.T, client *codersdk.Client, organization string, res *echo.Responses) coderd.ProvisionerJob {
func CreateProjectVersion(t *testing.T, client *codersdk.Client, organization string, res *echo.Responses) coderd.ProjectVersion {
data, err := echo.Tar(res)
require.NoError(t, err)
file, err := client.UploadFile(context.Background(), codersdk.ContentTypeTar, data)
file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data)
require.NoError(t, err)
job, err := client.CreateProjectImportJob(context.Background(), organization, coderd.CreateProjectImportJobRequest{
projectVersion, err := client.CreateProjectVersion(context.Background(), organization, coderd.CreateProjectVersionRequest{
StorageSource: file.Hash,
StorageMethod: database.ProvisionerStorageMethodFile,
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
return job
return projectVersion
}
// 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, job uuid.UUID) coderd.Project {
func CreateProject(t *testing.T, client *codersdk.Client, organization string, version uuid.UUID) coderd.Project {
project, err := client.CreateProject(context.Background(), organization, coderd.CreateProjectRequest{
Name: randomUsername(),
VersionImportJobID: job,
Name: randomUsername(),
VersionID: version,
})
require.NoError(t, err)
return project
}
// AwaitProjectImportJob awaits for an import job to reach completed status.
func AwaitProjectImportJob(t *testing.T, client *codersdk.Client, organization string, job uuid.UUID) coderd.ProvisionerJob {
var provisionerJob coderd.ProvisionerJob
func AwaitProjectVersionJob(t *testing.T, client *codersdk.Client, version uuid.UUID) coderd.ProjectVersion {
var projectVersion coderd.ProjectVersion
require.Eventually(t, func() bool {
var err error
provisionerJob, err = client.ProjectImportJob(context.Background(), organization, job)
projectVersion, err = client.ProjectVersion(context.Background(), version)
require.NoError(t, err)
return provisionerJob.Status.Completed()
return projectVersion.Job.CompletedAt != nil
}, 5*time.Second, 25*time.Millisecond)
return provisionerJob
return projectVersion
}
// AwaitWorkspaceProvisionJob awaits for a workspace provision job to reach completed status.
func AwaitWorkspaceProvisionJob(t *testing.T, client *codersdk.Client, organization string, job uuid.UUID) coderd.ProvisionerJob {
var provisionerJob coderd.ProvisionerJob
// 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
require.Eventually(t, func() bool {
var err error
provisionerJob, err = client.WorkspaceProvisionJob(context.Background(), organization, job)
workspaceBuild, err = client.WorkspaceBuild(context.Background(), build)
require.NoError(t, err)
return provisionerJob.Status.Completed()
return workspaceBuild.Job.CompletedAt != nil
}, 5*time.Second, 25*time.Millisecond)
return provisionerJob
return workspaceBuild
}
// 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
require.Eventually(t, func() bool {
var err error
resources, err = client.WorkspaceResourcesByBuild(context.Background(), build)
require.NoError(t, err)
for _, resource := range resources {
if resource.Agent == nil {
continue
}
if resource.Agent.UpdatedAt.IsZero() {
return false
}
}
return true
}, 5*time.Second, 25*time.Millisecond)
return resources
}
// CreateWorkspace creates a workspace for the user and project provided.

View File

@ -20,17 +20,18 @@ func TestMain(m *testing.M) {
func TestNew(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
user := coderdtest.CreateFirstUser(t, client)
closer := coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
history, err := client.CreateWorkspaceHistory(context.Background(), "me", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceProvisionJob(t, client, user.Organization, history.ProvisionJobID)
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
coderdtest.AwaitWorkspaceAgents(t, client, build.ID)
closer.Close()
}

View File

@ -2,11 +2,14 @@ package coderd
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/coder/coder/database"
@ -14,11 +17,12 @@ import (
"github.com/coder/coder/httpmw"
)
type UploadFileResponse struct {
// UploadResponse contains the hash to reference the uploaded file.
type UploadResponse struct {
Hash string `json:"hash"`
}
func (api *api) postUpload(rw http.ResponseWriter, r *http.Request) {
func (api *api) postFile(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
contentType := r.Header.Get("Content-Type")
@ -45,7 +49,7 @@ func (api *api) postUpload(rw http.ResponseWriter, r *http.Request) {
if err == nil {
// The file already exists!
render.Status(r, http.StatusOK)
render.JSON(rw, r, UploadFileResponse{
render.JSON(rw, r, UploadResponse{
Hash: file.Hash,
})
return
@ -64,7 +68,33 @@ func (api *api) postUpload(rw http.ResponseWriter, r *http.Request) {
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, UploadFileResponse{
render.JSON(rw, r, UploadResponse{
Hash: file.Hash,
})
}
func (api *api) fileByHash(rw http.ResponseWriter, r *http.Request) {
hash := chi.URLParam(r, "hash")
if hash == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "hash must be provided",
})
return
}
file, err := api.Database.GetFileByHash(r.Context(), hash)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: "no file exists with that hash",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get file: %s", err),
})
return
}
rw.Header().Set("Content-Type", file.Mimetype)
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write(file.Data)
}

View File

@ -2,6 +2,7 @@ package coderd_test
import (
"context"
"net/http"
"testing"
"github.com/stretchr/testify/require"
@ -10,32 +11,57 @@ import (
"github.com/coder/coder/codersdk"
)
func TestPostUpload(t *testing.T) {
func TestPostFiles(t *testing.T) {
t.Parallel()
t.Run("BadContentType", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.UploadFile(context.Background(), "bad", []byte{'a'})
_ = coderdtest.CreateFirstUser(t, client)
_, err := client.Upload(context.Background(), "bad", []byte{'a'})
require.Error(t, err)
})
t.Run("Insert", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.UploadFile(context.Background(), codersdk.ContentTypeTar, make([]byte, 1024))
_ = coderdtest.CreateFirstUser(t, client)
_, err := client.Upload(context.Background(), codersdk.ContentTypeTar, make([]byte, 1024))
require.NoError(t, err)
})
t.Run("InsertAlreadyExists", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
_ = coderdtest.CreateFirstUser(t, client)
data := make([]byte, 1024)
_, err := client.UploadFile(context.Background(), codersdk.ContentTypeTar, data)
_, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data)
require.NoError(t, err)
_, err = client.UploadFile(context.Background(), codersdk.ContentTypeTar, data)
_, err = client.Upload(context.Background(), codersdk.ContentTypeTar, data)
require.NoError(t, err)
})
}
func TestDownload(t *testing.T) {
t.Parallel()
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
_, _, err := client.Download(context.Background(), "something")
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("Insert", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
resp, err := client.Upload(context.Background(), codersdk.ContentTypeTar, make([]byte, 1024))
require.NoError(t, err)
data, contentType, err := client.Download(context.Background(), resp.Hash)
require.NoError(t, err)
require.Len(t, data, 1024)
require.Equal(t, codersdk.ContentTypeTar, contentType)
})
}

View File

@ -1,9 +1,21 @@
package coderd
import (
"database/sql"
"errors"
"fmt"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"golang.org/x/xerrors"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
"github.com/coder/coder/httpmw"
)
// Organization is the JSON representation of a Coder organization.
@ -14,6 +26,317 @@ type Organization struct {
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)
render.JSON(rw, r, convertOrganization(organization))
}
func (api *api) provisionerDaemonsByOrganization(rw http.ResponseWriter, r *http.Request) {
daemons, err := api.Database.GetProvisionerDaemons(r.Context())
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner daemons: %s", err),
})
return
}
if daemons == nil {
daemons = []database.ProvisionerDaemon{}
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, daemons)
}
// Creates a new version of a project. An import job is queued to parse the storage method provided.
func (api *api) postProjectVersionsByOrganization(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
organization := httpmw.OrganizationParam(r)
var req CreateProjectVersionRequest
if !httpapi.Read(rw, r, &req) {
return
}
if req.ProjectID != nil {
_, err := api.Database.GetProjectByID(r.Context(), *req.ProjectID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: "project does not exist",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project: %s", err),
})
return
}
}
file, err := api.Database.GetFileByHash(r.Context(), req.StorageSource)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: "file not found",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get file: %s", err),
})
return
}
var projectVersion database.ProjectVersion
var provisionerJob database.ProvisionerJob
err = api.Database.InTx(func(db database.Store) error {
jobID := uuid.New()
for _, parameterValue := range req.ParameterValues {
_, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
ID: uuid.New(),
Name: parameterValue.Name,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Scope: database.ParameterScopeImportJob,
ScopeID: jobID.String(),
SourceScheme: parameterValue.SourceScheme,
SourceValue: parameterValue.SourceValue,
DestinationScheme: parameterValue.DestinationScheme,
})
if err != nil {
return xerrors.Errorf("insert parameter value: %w", err)
}
}
provisionerJob, err = api.Database.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{
ID: jobID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
OrganizationID: organization.ID,
InitiatorID: apiKey.UserID,
Provisioner: req.Provisioner,
StorageMethod: database.ProvisionerStorageMethodFile,
StorageSource: file.Hash,
Type: database.ProvisionerJobTypeProjectVersionImport,
Input: []byte{'{', '}'},
})
if err != nil {
return xerrors.Errorf("insert provisioner job: %w", err)
}
var projectID uuid.NullUUID
if req.ProjectID != nil {
projectID = uuid.NullUUID{
UUID: *req.ProjectID,
Valid: true,
}
}
projectVersion, err = api.Database.InsertProjectVersion(r.Context(), database.InsertProjectVersionParams{
ID: uuid.New(),
ProjectID: projectID,
OrganizationID: organization.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Name: namesgenerator.GetRandomName(1),
Description: "",
JobID: provisionerJob.ID,
})
if err != nil {
return xerrors.Errorf("insert project version: %w", err)
}
return nil
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertProjectVersion(projectVersion, convertProvisionerJob(provisionerJob)))
}
// Create a new project in an organization.
func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Request) {
var createProject CreateProjectRequest
if !httpapi.Read(rw, r, &createProject) {
return
}
organization := httpmw.OrganizationParam(r)
_, err := api.Database.GetProjectByOrganizationAndName(r.Context(), database.GetProjectByOrganizationAndNameParams{
OrganizationID: organization.ID,
Name: createProject.Name,
})
if err == nil {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: fmt.Sprintf("project %q already exists", createProject.Name),
Errors: []httpapi.Error{{
Field: "name",
Code: "exists",
}},
})
return
}
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project by name: %s", err),
})
return
}
projectVersion, err := api.Database.GetProjectVersionByID(r.Context(), createProject.VersionID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: "project version does not exist",
})
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project version by id: %s", err),
})
return
}
importJob, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get import job by id: %s", err),
})
return
}
var project Project
err = api.Database.InTx(func(db database.Store) error {
dbProject, err := db.InsertProject(r.Context(), database.InsertProjectParams{
ID: uuid.New(),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
OrganizationID: organization.ID,
Name: createProject.Name,
Provisioner: importJob.Provisioner,
ActiveVersionID: projectVersion.ID,
})
if err != nil {
return xerrors.Errorf("insert project: %s", err)
}
err = db.UpdateProjectVersionByID(r.Context(), database.UpdateProjectVersionByIDParams{
ID: projectVersion.ID,
ProjectID: uuid.NullUUID{
UUID: dbProject.ID,
Valid: true,
},
})
if err != nil {
return xerrors.Errorf("insert project version: %s", err)
}
project = convertProject(dbProject, 0)
return nil
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, project)
}
func (api *api) projectsByOrganization(rw http.ResponseWriter, r *http.Request) {
organization := httpmw.OrganizationParam(r)
projects, err := api.Database.GetProjectsByOrganization(r.Context(), organization.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get projects: %s", err.Error()),
})
return
}
projectIDs := make([]uuid.UUID, 0, len(projects))
for _, project := range projects {
projectIDs = append(projectIDs, project.ID)
}
workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByProjectIDs(r.Context(), projectIDs)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace counts: %s", err.Error()),
})
return
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertProjects(projects, workspaceCounts))
}
func (api *api) projectByOrganizationAndName(rw http.ResponseWriter, r *http.Request) {
organization := httpmw.OrganizationParam(r)
projectName := chi.URLParam(r, "projectname")
project, err := api.Database.GetProjectByOrganizationAndName(r.Context(), database.GetProjectByOrganizationAndNameParams{
OrganizationID: organization.ID,
Name: projectName,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("no project found by name %q in the %q organization", projectName, organization.Name),
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project by organization and name: %s", err),
})
return
}
workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByProjectIDs(r.Context(), []uuid.UUID{project.ID})
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace counts: %s", err.Error()),
})
return
}
count := uint32(0)
if len(workspaceCounts) > 0 {
count = uint32(workspaceCounts[0].Count)
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertProject(project, count))
}
// convertOrganization consumes the database representation and outputs an API friendly representation.
func convertOrganization(organization database.Organization) Organization {
return Organization{

View File

@ -0,0 +1,179 @@
package coderd_test
import (
"context"
"net/http"
"testing"
"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"
"github.com/coder/coder/provisioner/echo"
)
func TestProvisionerDaemonsByOrganization(t *testing.T) {
t.Parallel()
t.Run("NoAuth", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.ProvisionerDaemonsByOrganization(context.Background(), "someorg")
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_, err := client.ProvisionerDaemonsByOrganization(context.Background(), user.OrganizationID)
require.NoError(t, err)
})
}
func TestPostProjectVersionsByOrganization(t *testing.T) {
t.Parallel()
t.Run("InvalidProject", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
projectID := uuid.New()
_, err := client.CreateProjectVersion(context.Background(), user.OrganizationID, coderd.CreateProjectVersionRequest{
ProjectID: &projectID,
StorageMethod: database.ProvisionerStorageMethodFile,
StorageSource: "hash",
Provisioner: database.ProvisionerTypeEcho,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("FileNotFound", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_, err := client.CreateProjectVersion(context.Background(), user.OrganizationID, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProvisionerStorageMethodFile,
StorageSource: "hash",
Provisioner: database.ProvisionerTypeEcho,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("WithParameters", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
data, err := echo.Tar(&echo.Responses{
Parse: echo.ParseComplete,
Provision: echo.ProvisionComplete,
ProvisionDryRun: echo.ProvisionComplete,
})
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{
StorageMethod: database.ProvisionerStorageMethodFile,
StorageSource: file.Hash,
Provisioner: database.ProvisionerTypeEcho,
ParameterValues: []coderd.CreateParameterRequest{{
Name: "example",
SourceValue: "value",
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
}},
})
require.NoError(t, err)
})
}
func TestPostProjectsByOrganization(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
})
t.Run("AlreadyExists", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
_, err := client.CreateProject(context.Background(), user.OrganizationID, coderd.CreateProjectRequest{
Name: project.Name,
VersionID: version.ID,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
t.Run("NoVersion", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_, err := client.CreateProject(context.Background(), user.OrganizationID, coderd.CreateProjectRequest{
Name: "test",
VersionID: uuid.New(),
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
}
func TestProjectsByOrganization(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
projects, err := client.ProjectsByOrganization(context.Background(), user.OrganizationID)
require.NoError(t, err)
require.NotNil(t, projects)
require.Len(t, projects, 0)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
projects, err := client.ProjectsByOrganization(context.Background(), user.OrganizationID)
require.NoError(t, err)
require.Len(t, projects, 1)
})
}
func TestProjectByOrganizationAndName(t *testing.T) {
t.Parallel()
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_, err := client.ProjectByName(context.Background(), user.OrganizationID, "something")
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("Found", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
_, err := client.ProjectByName(context.Background(), user.OrganizationID, project.Name)
require.NoError(t, err)
})
}

189
coderd/parameters.go Normal file
View File

@ -0,0 +1,189 @@
package coderd
import (
"database/sql"
"errors"
"fmt"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/google/uuid"
"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
if !httpapi.Read(rw, r, &createRequest) {
return
}
scope, scopeID, valid := readScopeAndID(rw, r)
if !valid {
return
}
_, err := api.Database.GetParameterValueByScopeAndName(r.Context(), database.GetParameterValueByScopeAndNameParams{
Scope: scope,
ScopeID: scopeID,
Name: createRequest.Name,
})
if err == nil {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: fmt.Sprintf("a parameter already exists in scope %q with name %q", scope, createRequest.Name),
})
return
}
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get parameter value: %s", err),
})
return
}
parameterValue, err := api.Database.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
ID: uuid.New(),
Name: createRequest.Name,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Scope: scope,
ScopeID: scopeID,
SourceScheme: createRequest.SourceScheme,
SourceValue: createRequest.SourceValue,
DestinationScheme: createRequest.DestinationScheme,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert parameter value: %s", err),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertParameterValue(parameterValue))
}
func (api *api) parameters(rw http.ResponseWriter, r *http.Request) {
scope, scopeID, valid := readScopeAndID(rw, r)
if !valid {
return
}
parameterValues, err := api.Database.GetParameterValuesByScope(r.Context(), database.GetParameterValuesByScopeParams{
Scope: scope,
ScopeID: scopeID,
})
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get parameter values by scope: %s", err),
})
return
}
apiParameterValues := make([]Parameter, 0, len(parameterValues))
for _, parameterValue := range parameterValues {
apiParameterValues = append(apiParameterValues, convertParameterValue(parameterValue))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiParameterValues)
}
func (api *api) deleteParameter(rw http.ResponseWriter, r *http.Request) {
scope, scopeID, valid := readScopeAndID(rw, r)
if !valid {
return
}
name := chi.URLParam(r, "name")
parameterValue, err := api.Database.GetParameterValueByScopeAndName(r.Context(), database.GetParameterValueByScopeAndNameParams{
Scope: scope,
ScopeID: scopeID,
Name: name,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("parameter doesn't exist in the provided scope with name %q", name),
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get parameter value: %s", err),
})
return
}
err = api.Database.DeleteParameterValueByID(r.Context(), parameterValue.ID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("delete parameter: %s", err),
})
return
}
httpapi.Write(rw, http.StatusOK, httpapi.Response{
Message: "parameter deleted",
})
}
func convertParameterValue(parameterValue database.ParameterValue) Parameter {
return Parameter{
ID: parameterValue.ID,
CreatedAt: parameterValue.CreatedAt,
UpdatedAt: parameterValue.UpdatedAt,
Scope: ParameterScope(parameterValue.Scope),
ScopeID: parameterValue.ScopeID,
Name: parameterValue.Name,
SourceScheme: parameterValue.SourceScheme,
DestinationScheme: parameterValue.DestinationScheme,
}
}
func readScopeAndID(rw http.ResponseWriter, r *http.Request) (database.ParameterScope, string, bool) {
var scope database.ParameterScope
switch chi.URLParam(r, "scope") {
case string(ParameterOrganization):
scope = database.ParameterScopeOrganization
case string(ParameterProject):
scope = database.ParameterScopeProject
case string(ParameterUser):
scope = database.ParameterScopeUser
case string(ParameterWorkspace):
scope = database.ParameterScopeWorkspace
default:
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("invalid scope %q", scope),
})
return scope, "", false
}
return scope, chi.URLParam(r, "id"), true
}

121
coderd/parameters_test.go Normal file
View File

@ -0,0 +1,121 @@
package coderd_test
import (
"context"
"net/http"
"testing"
"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"
)
func TestPostParameter(t *testing.T) {
t.Parallel()
t.Run("BadScope", func(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{
Name: "example",
SourceValue: "tomato",
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
})
t.Run("Create", 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{
Name: "example",
SourceValue: "tomato",
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
})
require.NoError(t, err)
})
t.Run("AlreadyExists", 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{
Name: "example",
SourceValue: "tomato",
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
})
require.NoError(t, err)
_, err = client.CreateParameter(context.Background(), coderd.ParameterOrganization, user.OrganizationID, coderd.CreateParameterRequest{
Name: "example",
SourceValue: "tomato",
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
}
func TestParameters(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_, err := client.Parameters(context.Background(), coderd.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{
Name: "example",
SourceValue: "tomato",
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
})
require.NoError(t, err)
params, err := client.Parameters(context.Background(), coderd.ParameterOrganization, user.OrganizationID)
require.NoError(t, err)
require.Len(t, params, 1)
})
}
func TestDeleteParameter(t *testing.T) {
t.Parallel()
t.Run("NotExist", func(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")
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("Delete", func(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{
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)
require.NoError(t, err)
})
}

View File

@ -1,182 +0,0 @@
package coderd
import (
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/coder/coder/coderd/parameter"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
"github.com/coder/coder/httpmw"
)
// ParameterSchema represents a parameter parsed from project version source.
type ParameterSchema database.ParameterSchema
// ComputedParameterValue represents a computed parameter value.
type ComputedParameterValue parameter.ComputedValue
// CreateProjectImportJobRequest provides options to create a project import job.
type CreateProjectImportJobRequest struct {
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 []CreateParameterValueRequest `json:"parameter_values"`
}
// Create a new project import job!
func (api *api) postProjectImportByOrganization(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
organization := httpmw.OrganizationParam(r)
var req CreateProjectImportJobRequest
if !httpapi.Read(rw, r, &req) {
return
}
file, err := api.Database.GetFileByHash(r.Context(), req.StorageSource)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "file not found",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get file: %s", err),
})
return
}
jobID := uuid.New()
for _, parameterValue := range req.ParameterValues {
_, err = api.Database.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
ID: uuid.New(),
Name: parameterValue.Name,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Scope: database.ParameterScopeImportJob,
ScopeID: jobID.String(),
SourceScheme: parameterValue.SourceScheme,
SourceValue: parameterValue.SourceValue,
DestinationScheme: parameterValue.DestinationScheme,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert parameter value: %s", err),
})
return
}
}
job, err := api.Database.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{
ID: jobID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
OrganizationID: organization.ID,
InitiatorID: apiKey.UserID,
Provisioner: req.Provisioner,
StorageMethod: database.ProvisionerStorageMethodFile,
StorageSource: file.Hash,
Type: database.ProvisionerJobTypeProjectVersionImport,
Input: []byte{'{', '}'},
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert provisioner job: %s", err),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertProvisionerJob(job))
}
// Returns imported parameter schemas from a completed job!
func (api *api) projectImportJobSchemasByID(rw http.ResponseWriter, r *http.Request) {
job := httpmw.ProvisionerJobParam(r)
if !convertProvisionerJob(job).Status.Completed() {
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: "Job hasn't completed!",
})
return
}
schemas, err := api.Database.GetParameterSchemasByJobID(r.Context(), job.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("list parameter schemas: %s", err),
})
return
}
if schemas == nil {
schemas = []database.ParameterSchema{}
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, schemas)
}
// Returns computed parameters for an import job by ID.
func (api *api) projectImportJobParametersByID(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
job := httpmw.ProvisionerJobParam(r)
if !convertProvisionerJob(job).Status.Completed() {
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: "Job hasn't completed!",
})
return
}
values, err := parameter.Compute(r.Context(), api.Database, parameter.ComputeScope{
ProjectImportJobID: job.ID,
OrganizationID: job.OrganizationID,
UserID: apiKey.UserID,
}, &parameter.ComputeOptions{
// We *never* want to send the client secret parameter values.
HideRedisplayValues: true,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("compute values: %s", err),
})
return
}
if values == nil {
values = []parameter.ComputedValue{}
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, values)
}
// Returns resources for an import job by ID.
func (api *api) projectImportJobResourcesByID(rw http.ResponseWriter, r *http.Request) {
job := httpmw.ProvisionerJobParam(r)
if !convertProvisionerJob(job).Status.Completed() {
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: "Job hasn't completed!",
})
return
}
resources, err := api.Database.GetProvisionerJobResourcesByJobID(r.Context(), job.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project import job resources: %s", err),
})
return
}
if resources == nil {
resources = []database.ProvisionerJobResource{}
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, resources)
}

View File

@ -1,163 +0,0 @@
package coderd_test
import (
"context"
"net/http"
"testing"
"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"
)
func TestPostProjectImportByOrganization(t *testing.T) {
t.Parallel()
t.Run("FileNotFound", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
_, err := client.CreateProjectImportJob(context.Background(), user.Organization, coderd.CreateProjectImportJobRequest{
StorageMethod: database.ProvisionerStorageMethodFile,
StorageSource: "bananas",
Provisioner: database.ProvisionerTypeEcho,
})
require.Error(t, err)
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
})
}
func TestProjectImportJobSchemasByID(t *testing.T) {
t.Parallel()
t.Run("ListRunning", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
_, err := client.ProjectImportJobSchemas(context.Background(), user.Organization, job.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{
Parse: []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{
ParameterSchemas: []*proto.ParameterSchema{{
Name: "example",
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
},
}},
},
},
}},
Provision: echo.ProvisionComplete,
})
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
schemas, err := client.ProjectImportJobSchemas(context.Background(), user.Organization, job.ID)
require.NoError(t, err)
require.NotNil(t, schemas)
require.Len(t, schemas, 1)
})
}
func TestProjectImportJobParametersByID(t *testing.T) {
t.Parallel()
t.Run("ListRunning", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
_, err := client.ProjectImportJobSchemas(context.Background(), user.Organization, job.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{
Parse: []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{
ParameterSchemas: []*proto.ParameterSchema{{
Name: "example",
RedisplayValue: true,
DefaultSource: &proto.ParameterSource{
Scheme: proto.ParameterSource_DATA,
Value: "hello",
},
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
},
}},
},
},
}},
Provision: echo.ProvisionComplete,
})
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
params, err := client.ProjectImportJobParameters(context.Background(), user.Organization, job.ID)
require.NoError(t, err)
require.NotNil(t, params)
require.Len(t, params, 1)
require.Equal(t, "hello", params[0].SourceValue)
})
}
func TestProjectImportJobResourcesByID(t *testing.T) {
t.Parallel()
t.Run("ListRunning", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
_, err := client.ProjectImportJobResources(context.Background(), user.Organization, job.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
}},
},
},
}},
})
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
resources, err := client.ProjectImportJobResources(context.Background(), user.Organization, job.ID)
require.NoError(t, err)
require.NotNil(t, resources)
require.Len(t, resources, 2)
require.Equal(t, "some", resources[0].Name)
require.Equal(t, "example", resources[0].Type)
})
}

View File

@ -7,27 +7,15 @@ import (
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"golang.org/x/xerrors"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
"github.com/coder/coder/httpmw"
)
// ParameterValue represents a set value for the scope.
type ParameterValue database.ParameterValue
// CreateParameterValueRequest is used to create a new parameter value for a scope.
type CreateParameterValueRequest 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"`
}
// 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.
@ -42,48 +30,10 @@ type Project struct {
WorkspaceOwnerCount uint32 `json:"workspace_owner_count"`
}
// CreateProjectRequest enables callers to create a new Project.
type CreateProjectRequest struct {
Name string `json:"name" validate:"username,required"`
// VersionImportJobID 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
// the project works. There is no reason the data-model cannot support
// empty projects, but it doesn't make sense for users.
VersionImportJobID uuid.UUID `json:"import_job_id" validate:"required"`
}
// Lists all projects the authenticated user has access to.
func (api *api) projects(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), apiKey.UserID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organizations: %s", err.Error()),
})
return
}
organizationIDs := make([]string, 0, len(organizations))
for _, organization := range organizations {
organizationIDs = append(organizationIDs, organization.ID)
}
projects, err := api.Database.GetProjectsByOrganizationIDs(r.Context(), organizationIDs)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get projects: %s", err.Error()),
})
return
}
projectIDs := make([]uuid.UUID, 0, len(projects))
for _, project := range projects {
projectIDs = append(projectIDs, project.ID)
}
workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByProjectIDs(r.Context(), projectIDs)
// Returns a single project.
func (api *api) project(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)
workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByProjectIDs(r.Context(), []uuid.UUID{project.ID})
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
@ -93,184 +43,91 @@ func (api *api) projects(rw http.ResponseWriter, r *http.Request) {
})
return
}
count := uint32(0)
if len(workspaceCounts) > 0 {
count = uint32(workspaceCounts[0].Count)
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertProjects(projects, workspaceCounts))
render.JSON(rw, r, convertProject(project, count))
}
// Lists all projects in an organization.
func (api *api) projectsByOrganization(rw http.ResponseWriter, r *http.Request) {
organization := httpmw.OrganizationParam(r)
projects, err := api.Database.GetProjectsByOrganizationIDs(r.Context(), []string{organization.ID})
func (api *api) projectVersionsByProject(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)
versions, err := api.Database.GetProjectVersionsByProjectID(r.Context(), project.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get projects: %s", err.Error()),
Message: fmt.Sprintf("get project version: %s", err),
})
return
}
projectIDs := make([]uuid.UUID, 0, len(projects))
for _, project := range projects {
projectIDs = append(projectIDs, project.ID)
}
workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByProjectIDs(r.Context(), projectIDs)
if errors.Is(err, sql.ErrNoRows) {
err = nil
jobIDs := make([]uuid.UUID, 0, len(versions))
for _, version := range versions {
jobIDs = append(jobIDs, version.JobID)
}
jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace counts: %s", err.Error()),
Message: fmt.Sprintf("get jobs: %s", err),
})
return
}
jobByID := map[string]database.ProvisionerJob{}
for _, job := range jobs {
jobByID[job.ID.String()] = job
}
apiVersion := make([]ProjectVersion, 0)
for _, version := range versions {
job, exists := jobByID[version.JobID.String()]
if !exists {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("job %q doesn't exist for version %q", version.JobID, version.ID),
})
return
}
apiVersion = append(apiVersion, convertProjectVersion(version, convertProvisionerJob(job)))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertProjects(projects, workspaceCounts))
render.JSON(rw, r, apiVersion)
}
// Create a new project in an organization.
func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Request) {
var createProject CreateProjectRequest
if !httpapi.Read(rw, r, &createProject) {
return
}
organization := httpmw.OrganizationParam(r)
_, err := api.Database.GetProjectByOrganizationAndName(r.Context(), database.GetProjectByOrganizationAndNameParams{
OrganizationID: organization.ID,
Name: createProject.Name,
func (api *api) projectVersionByName(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)
projectVersionName := chi.URLParam(r, "projectversionname")
projectVersion, err := api.Database.GetProjectVersionByProjectIDAndName(r.Context(), database.GetProjectVersionByProjectIDAndNameParams{
ProjectID: uuid.NullUUID{
UUID: project.ID,
Valid: true,
},
Name: projectVersionName,
})
if err == nil {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: fmt.Sprintf("project %q already exists", createProject.Name),
Errors: []httpapi.Error{{
Field: "name",
Code: "exists",
}},
})
return
}
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project by name: %s", err),
})
return
}
importJob, err := api.Database.GetProvisionerJobByID(r.Context(), createProject.VersionImportJobID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: "import job does not exist",
Message: fmt.Sprintf("no project version found by name %q", projectVersionName),
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get import job by id: %s", err),
Message: fmt.Sprintf("get project version by name: %s", err),
})
return
}
var project Project
err = api.Database.InTx(func(db database.Store) error {
projectVersionID := uuid.New()
dbProject, err := db.InsertProject(r.Context(), database.InsertProjectParams{
ID: uuid.New(),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
OrganizationID: organization.ID,
Name: createProject.Name,
Provisioner: importJob.Provisioner,
ActiveVersionID: projectVersionID,
})
if err != nil {
return xerrors.Errorf("insert project: %s", err)
}
_, err = db.InsertProjectVersion(r.Context(), database.InsertProjectVersionParams{
ID: projectVersionID,
ProjectID: dbProject.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Name: namesgenerator.GetRandomName(1),
ImportJobID: importJob.ID,
})
if err != nil {
return xerrors.Errorf("insert project version: %s", err)
}
project = convertProject(dbProject, 0)
return nil
})
job, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, project)
}
// Returns a single project.
func (*api) projectByOrganization(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)
render.Status(r, http.StatusOK)
render.JSON(rw, r, project)
}
// Creates parameters for a project.
// This should validate the calling user has permissions!
func (api *api) postParametersByProject(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)
var createRequest CreateParameterValueRequest
if !httpapi.Read(rw, r, &createRequest) {
return
}
parameterValue, err := api.Database.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
ID: uuid.New(),
Name: createRequest.Name,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Scope: database.ParameterScopeProject,
ScopeID: project.ID.String(),
SourceScheme: createRequest.SourceScheme,
SourceValue: createRequest.SourceValue,
DestinationScheme: createRequest.DestinationScheme,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert parameter value: %s", err),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, parameterValue)
}
// Lists parameters for a project.
func (api *api) parametersByProject(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)
parameterValues, err := api.Database.GetParameterValuesByScope(r.Context(), database.GetParameterValuesByScopeParams{
Scope: database.ParameterScopeProject,
ScopeID: project.ID.String(),
})
if errors.Is(err, sql.ErrNoRows) {
err = nil
parameterValues = []database.ParameterValue{}
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get parameter values: %s", err),
})
return
}
apiParameterValues := make([]ParameterValue, 0, len(parameterValues))
for _, parameterValue := range parameterValues {
apiParameterValues = append(apiParameterValues, convertParameterValue(parameterValue))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiParameterValues)
render.JSON(rw, r, convertProjectVersion(projectVersion, convertProvisionerJob(job)))
}
func convertProjects(projects []database.Project, workspaceCounts []database.GetWorkspaceOwnerCountsByProjectIDsRow) []Project {
@ -304,8 +161,3 @@ func convertProject(project database.Project, workspaceOwnerCount uint32) Projec
WorkspaceOwnerCount: workspaceOwnerCount,
}
}
func convertParameterValue(parameterValue database.ParameterValue) ParameterValue {
parameterValue.SourceValue = ""
return ParameterValue(parameterValue)
}

View File

@ -7,163 +7,59 @@ 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"
)
func TestProjects(t *testing.T) {
func TestProject(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
projects, err := client.Projects(context.Background(), "")
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
_, err := client.Project(context.Background(), project.ID)
require.NoError(t, err)
require.NotNil(t, projects)
require.Len(t, projects, 0)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
_ = coderdtest.CreateProject(t, client, user.Organization, job.ID)
projects, err := client.Projects(context.Background(), "")
require.NoError(t, err)
require.Len(t, projects, 1)
})
t.Run("ListWorkspaceOwnerCount", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_ = coderdtest.CreateWorkspace(t, client, "", project.ID)
_ = coderdtest.CreateWorkspace(t, client, "", project.ID)
projects, err := client.Projects(context.Background(), "")
require.NoError(t, err)
require.Len(t, projects, 1)
require.Equal(t, projects[0].WorkspaceOwnerCount, uint32(1))
})
}
func TestProjectsByOrganization(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
projects, err := client.Projects(context.Background(), user.Organization)
require.NoError(t, err)
require.NotNil(t, projects)
require.Len(t, projects, 0)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
_ = coderdtest.CreateProject(t, client, user.Organization, job.ID)
projects, err := client.Projects(context.Background(), "")
require.NoError(t, err)
require.Len(t, projects, 1)
})
}
func TestPostProjectsByOrganization(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
_ = coderdtest.CreateProject(t, client, user.Organization, job.ID)
})
t.Run("AlreadyExists", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_, err := client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: project.Name,
VersionImportJobID: job.ID,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
}
func TestProjectByOrganization(t *testing.T) {
func TestProjectVersionsByProject(t *testing.T) {
t.Parallel()
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_, err := client.Project(context.Background(), user.Organization, project.Name)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
versions, err := client.ProjectVersionsByProject(context.Background(), project.ID)
require.NoError(t, err)
require.Len(t, versions, 1)
})
}
func TestPostParametersByProject(t *testing.T) {
func TestProjectVersionByName(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_, err := client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{
Name: "somename",
SourceValue: "tomato",
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable,
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
_, err := client.ProjectVersionByName(context.Background(), project.ID, "nothing")
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("Found", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
_, err := client.ProjectVersionByName(context.Background(), project.ID, version.Name)
require.NoError(t, err)
})
}
func TestParametersByProject(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
params, err := client.ProjectParameters(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.NotNil(t, params)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_, err := client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{
Name: "example",
SourceValue: "source-value",
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable,
})
require.NoError(t, err)
params, err := client.ProjectParameters(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.NotNil(t, params)
require.Len(t, params, 1)
})
}

View File

@ -1,113 +0,0 @@
package coderd
import (
"database/sql"
"errors"
"fmt"
"net/http"
"time"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"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"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Name string `json:"name"`
ImportJobID uuid.UUID `json:"import_job_id"`
}
// CreateProjectVersionRequest enables callers to create a new Project Version.
type CreateProjectVersionRequest struct {
ImportJobID uuid.UUID `json:"import_job_id" validate:"required"`
}
// Lists versions for a single project.
func (api *api) projectVersionsByOrganization(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)
version, err := api.Database.GetProjectVersionsByProjectID(r.Context(), project.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project version: %s", err),
})
return
}
apiVersion := make([]ProjectVersion, 0)
for _, version := range version {
apiVersion = append(apiVersion, convertProjectVersion(version))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiVersion)
}
// Return a single project version by organization and name.
func (*api) projectVersionByOrganizationAndName(rw http.ResponseWriter, r *http.Request) {
projectVersion := httpmw.ProjectVersionParam(r)
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertProjectVersion(projectVersion))
}
// Creates a new version of the project. An import job is queued to parse
// the storage method provided. Once completed, the import job will specify
// the version as latest.
func (api *api) postProjectVersionByOrganization(rw http.ResponseWriter, r *http.Request) {
var createProjectVersion CreateProjectVersionRequest
if !httpapi.Read(rw, r, &createProjectVersion) {
return
}
job, err := api.Database.GetProvisionerJobByID(r.Context(), createProjectVersion.ImportJobID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: "job not found",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
project := httpmw.ProjectParam(r)
projectVersion, err := api.Database.InsertProjectVersion(r.Context(), database.InsertProjectVersionParams{
ID: uuid.New(),
ProjectID: project.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Name: namesgenerator.GetRandomName(1),
ImportJobID: job.ID,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert project version: %s", err),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertProjectVersion(projectVersion))
}
func convertProjectVersion(version database.ProjectVersion) ProjectVersion {
return ProjectVersion{
ID: version.ID,
ProjectID: version.ProjectID,
CreatedAt: version.CreatedAt,
UpdatedAt: version.UpdatedAt,
Name: version.Name,
ImportJobID: version.ImportJobID,
}
}

View File

@ -1,54 +0,0 @@
package coderd_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
)
func TestProjectVersionsByOrganization(t *testing.T) {
t.Parallel()
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
versions, err := client.ProjectVersions(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.NotNil(t, versions)
require.Len(t, versions, 1)
})
}
func TestProjectVersionByOrganizationAndName(t *testing.T) {
t.Parallel()
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_, err := client.ProjectVersion(context.Background(), user.Organization, project.Name, project.ActiveVersionID.String())
require.NoError(t, err)
})
}
func TestPostProjectVersionByOrganization(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_, err := client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
ImportJobID: job.ID,
})
require.NoError(t, err)
})
}

150
coderd/projectversions.go Normal file
View File

@ -0,0 +1,150 @@
package coderd
import (
"database/sql"
"errors"
"fmt"
"net/http"
"time"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/coder/coder/coderd/parameter"
"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)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertProjectVersion(projectVersion, convertProvisionerJob(job)))
}
func (api *api) projectVersionSchema(rw http.ResponseWriter, r *http.Request) {
projectVersion := httpmw.ProjectVersionParam(r)
job, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
if !job.CompletedAt.Valid {
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: "Project version job hasn't completed!",
})
return
}
schemas, err := api.Database.GetParameterSchemasByJobID(r.Context(), job.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("list parameter schemas: %s", err),
})
return
}
if schemas == nil {
schemas = []database.ParameterSchema{}
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, schemas)
}
func (api *api) projectVersionParameters(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
projectVersion := httpmw.ProjectVersionParam(r)
job, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
if !job.CompletedAt.Valid {
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: "Job hasn't completed!",
})
return
}
values, err := parameter.Compute(r.Context(), api.Database, parameter.ComputeScope{
ProjectImportJobID: job.ID,
OrganizationID: job.OrganizationID,
UserID: apiKey.UserID,
}, &parameter.ComputeOptions{
// We *never* want to send the client secret parameter values.
HideRedisplayValues: true,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("compute values: %s", err),
})
return
}
if values == nil {
values = []parameter.ComputedValue{}
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, values)
}
func (api *api) projectVersionResources(rw http.ResponseWriter, r *http.Request) {
projectVersion := httpmw.ProjectVersionParam(r)
job, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
api.provisionerJobResources(rw, r, job)
}
func (api *api) projectVersionLogs(rw http.ResponseWriter, r *http.Request) {
projectVersion := httpmw.ProjectVersionParam(r)
job, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
api.provisionerJobLogs(rw, r, job)
}
func convertProjectVersion(version database.ProjectVersion, job ProvisionerJob) ProjectVersion {
return ProjectVersion{
ID: version.ID,
ProjectID: &version.ProjectID.UUID,
CreatedAt: version.CreatedAt,
UpdatedAt: version.UpdatedAt,
Name: version.Name,
Job: job,
}
}

View File

@ -0,0 +1,205 @@
package coderd_test
import (
"context"
"net/http"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
)
func TestProjectVersion(t *testing.T) {
t.Parallel()
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
_, err := client.ProjectVersion(context.Background(), version.ID)
require.NoError(t, err)
})
}
func TestProjectVersionSchema(t *testing.T) {
t.Parallel()
t.Run("ListRunning", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
_, err := client.ProjectVersionSchema(context.Background(), version.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{
ParameterSchemas: []*proto.ParameterSchema{{
Name: "example",
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
},
}},
},
},
}},
Provision: echo.ProvisionComplete,
})
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
schemas, err := client.ProjectVersionSchema(context.Background(), version.ID)
require.NoError(t, err)
require.NotNil(t, schemas)
require.Len(t, schemas, 1)
})
}
func TestProjectVersionParameters(t *testing.T) {
t.Parallel()
t.Run("ListRunning", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
_, err := client.ProjectVersionParameters(context.Background(), version.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{
ParameterSchemas: []*proto.ParameterSchema{{
Name: "example",
RedisplayValue: true,
DefaultSource: &proto.ParameterSource{
Scheme: proto.ParameterSource_DATA,
Value: "hello",
},
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
},
}},
},
},
}},
Provision: echo.ProvisionComplete,
})
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
params, err := client.ProjectVersionParameters(context.Background(), version.ID)
require.NoError(t, err)
require.NotNil(t, params)
require.Len(t, params, 1)
require.Equal(t, "hello", params[0].SourceValue)
})
}
func TestProjectVersionResources(t *testing.T) {
t.Parallel()
t.Run("ListRunning", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
_, err := client.ProjectVersionResources(context.Background(), version.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
Agent: &proto.Agent{
Id: "something",
Auth: &proto.Agent_Token{},
},
}, {
Name: "another",
Type: "example",
}},
},
},
}},
})
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
resources, err := client.ProjectVersionResources(context.Background(), version.ID)
require.NoError(t, err)
require.NotNil(t, resources)
require.Len(t, resources, 4)
require.Equal(t, "some", resources[0].Name)
require.Equal(t, "example", resources[0].Type)
require.NotNil(t, resources[0].Agent)
})
}
func TestProjectVersionLogs(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
before := time.Now()
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "example",
},
},
}, {
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
Agent: &proto.Agent{
Id: "something",
Auth: &proto.Agent_Token{
Token: uuid.NewString(),
},
},
}, {
Name: "another",
Type: "example",
}},
},
},
}},
})
ctx, cancelFunc := context.WithCancel(context.Background())
t.Cleanup(cancelFunc)
logs, err := client.ProjectVersionLogsAfter(ctx, version.ID, before)
require.NoError(t, err)
log := <-logs
require.Equal(t, "example", log.Output)
}

View File

@ -12,7 +12,6 @@ import (
"reflect"
"time"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/hashicorp/yamux"
"github.com/moby/moby/pkg/namesgenerator"
@ -33,27 +32,8 @@ import (
type ProvisionerDaemon database.ProvisionerDaemon
// Lists all registered provisioner daemons.
func (api *api) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
daemons, err := api.Database.GetProvisionerDaemons(r.Context())
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner daemons: %s", err),
})
return
}
if daemons == nil {
daemons = []database.ProvisionerDaemon{}
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, daemons)
}
// Serves the provisioner daemon protobuf API over a WebSocket.
func (api *api) provisionerDaemonsServe(rw http.ResponseWriter, r *http.Request) {
func (api *api) provisionerDaemonsListen(rw http.ResponseWriter, r *http.Request) {
api.websocketWaitGroup.Add(1)
defer api.websocketWaitGroup.Done()
@ -113,8 +93,8 @@ func (api *api) provisionerDaemonsServe(rw http.ResponseWriter, r *http.Request)
// The input for a "workspace_provision" job.
type workspaceProvisionJob struct {
WorkspaceHistoryID uuid.UUID `json:"workspace_history_id"`
DryRun bool `json:"dry_run"`
WorkspaceBuildID uuid.UUID `json:"workspace_build_id"`
DryRun bool `json:"dry_run"`
}
// Implementation of the provisioner daemon protobuf server.
@ -182,32 +162,32 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty
UserName: user.Username,
}
switch job.Type {
case database.ProvisionerJobTypeWorkspaceProvision:
case database.ProvisionerJobTypeWorkspaceBuild:
var input workspaceProvisionJob
err = json.Unmarshal(job.Input, &input)
if err != nil {
return nil, failJob(fmt.Sprintf("unmarshal job input %q: %s", job.Input, err))
}
workspaceHistory, err := server.Database.GetWorkspaceHistoryByID(ctx, input.WorkspaceHistoryID)
workspaceBuild, err := server.Database.GetWorkspaceBuildByID(ctx, input.WorkspaceBuildID)
if err != nil {
return nil, failJob(fmt.Sprintf("get workspace history: %s", err))
return nil, failJob(fmt.Sprintf("get workspace build: %s", err))
}
workspace, err := server.Database.GetWorkspaceByID(ctx, workspaceHistory.WorkspaceID)
workspace, err := server.Database.GetWorkspaceByID(ctx, workspaceBuild.WorkspaceID)
if err != nil {
return nil, failJob(fmt.Sprintf("get workspace: %s", err))
}
projectVersion, err := server.Database.GetProjectVersionByID(ctx, workspaceHistory.ProjectVersionID)
projectVersion, err := server.Database.GetProjectVersionByID(ctx, workspaceBuild.ProjectVersionID)
if err != nil {
return nil, failJob(fmt.Sprintf("get project version: %s", err))
}
project, err := server.Database.GetProjectByID(ctx, projectVersion.ProjectID)
project, err := server.Database.GetProjectByID(ctx, projectVersion.ProjectID.UUID)
if err != nil {
return nil, failJob(fmt.Sprintf("get project: %s", err))
}
// Compute parameters for the workspace to consume.
parameters, err := parameter.Compute(ctx, server.Database, parameter.ComputeScope{
ProjectImportJobID: projectVersion.ImportJobID,
ProjectImportJobID: projectVersion.JobID,
OrganizationID: job.OrganizationID,
ProjectID: uuid.NullUUID{
UUID: project.ID,
@ -231,17 +211,17 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty
}
protoParameters = append(protoParameters, converted)
}
transition, err := convertWorkspaceTransition(workspaceHistory.Transition)
transition, err := convertWorkspaceTransition(workspaceBuild.Transition)
if err != nil {
return nil, failJob(fmt.Sprint("convert workspace transition: %w", err))
}
protoJob.Type = &proto.AcquiredJob_WorkspaceProvision_{
WorkspaceProvision: &proto.AcquiredJob_WorkspaceProvision{
WorkspaceHistoryId: workspaceHistory.ID.String(),
WorkspaceName: workspace.Name,
State: workspaceHistory.ProvisionerState,
ParameterValues: protoParameters,
protoJob.Type = &proto.AcquiredJob_WorkspaceBuild_{
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
WorkspaceBuildId: workspaceBuild.ID.String(),
WorkspaceName: workspace.Name,
State: workspaceBuild.ProvisionerState,
ParameterValues: protoParameters,
Metadata: &sdkproto.Provision_Metadata{
CoderUrl: server.AccessURL.String(),
WorkspaceTransition: transition,
@ -432,8 +412,8 @@ func (server *provisionerdServer) FailJob(ctx context.Context, failJob *proto.Fa
return nil, xerrors.Errorf("update provisioner job: %w", err)
}
switch jobType := failJob.Type.(type) {
case *proto.FailedJob_WorkspaceProvision_:
if jobType.WorkspaceProvision.State == nil {
case *proto.FailedJob_WorkspaceBuild_:
if jobType.WorkspaceBuild.State == nil {
break
}
var input workspaceProvisionJob
@ -441,13 +421,13 @@ func (server *provisionerdServer) FailJob(ctx context.Context, failJob *proto.Fa
if err != nil {
return nil, xerrors.Errorf("unmarshal workspace provision input: %w", err)
}
err = server.Database.UpdateWorkspaceHistoryByID(ctx, database.UpdateWorkspaceHistoryByIDParams{
err = server.Database.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
ID: jobID,
UpdatedAt: database.Now(),
ProvisionerState: jobType.WorkspaceProvision.State,
ProvisionerState: jobType.WorkspaceBuild.State,
})
if err != nil {
return nil, xerrors.Errorf("update workspace history state: %w", err)
return nil, xerrors.Errorf("update workspace build state: %w", err)
}
case *proto.FailedJob_ProjectImport_:
}
@ -481,7 +461,7 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
slog.F("resource_type", resource.Type),
slog.F("transition", transition))
err = insertProvisionerJobResource(ctx, server.Database, jobID, transition, resource)
err = insertWorkspaceResource(ctx, server.Database, jobID, transition, resource)
if err != nil {
return nil, xerrors.Errorf("insert resource: %w", err)
}
@ -503,16 +483,16 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
if err != nil {
return nil, xerrors.Errorf("complete job: %w", err)
}
case *proto.CompletedJob_WorkspaceProvision_:
case *proto.CompletedJob_WorkspaceBuild_:
var input workspaceProvisionJob
err = json.Unmarshal(job.Input, &input)
if err != nil {
return nil, xerrors.Errorf("unmarshal job data: %w", err)
}
workspaceHistory, err := server.Database.GetWorkspaceHistoryByID(ctx, input.WorkspaceHistoryID)
workspaceBuild, err := server.Database.GetWorkspaceBuildByID(ctx, input.WorkspaceBuildID)
if err != nil {
return nil, xerrors.Errorf("get workspace history: %w", err)
return nil, xerrors.Errorf("get workspace build: %w", err)
}
err = server.Database.InTx(func(db database.Store) error {
@ -527,17 +507,17 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
if err != nil {
return xerrors.Errorf("update provisioner job: %w", err)
}
err = db.UpdateWorkspaceHistoryByID(ctx, database.UpdateWorkspaceHistoryByIDParams{
ID: workspaceHistory.ID,
err = db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
ID: workspaceBuild.ID,
UpdatedAt: database.Now(),
ProvisionerState: jobType.WorkspaceProvision.State,
ProvisionerState: jobType.WorkspaceBuild.State,
})
if err != nil {
return xerrors.Errorf("update workspace history: %w", err)
return xerrors.Errorf("update workspace build: %w", err)
}
// This could be a bulk insert to improve performance.
for _, protoResource := range jobType.WorkspaceProvision.Resources {
err = insertProvisionerJobResource(ctx, db, job.ID, workspaceHistory.Transition, protoResource)
for _, protoResource := range jobType.WorkspaceBuild.Resources {
err = insertWorkspaceResource(ctx, db, job.ID, workspaceBuild.Transition, protoResource)
if err != nil {
return xerrors.Errorf("insert provisioner job: %w", err)
}
@ -555,8 +535,8 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
return &proto.Empty{}, nil
}
func insertProvisionerJobResource(ctx context.Context, db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, protoResource *sdkproto.Resource) error {
resource, err := db.InsertProvisionerJobResource(ctx, database.InsertProvisionerJobResourceParams{
func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, protoResource *sdkproto.Resource) error {
resource, err := db.InsertWorkspaceResource(ctx, database.InsertWorkspaceResourceParams{
ID: uuid.New(),
CreatedAt: database.Now(),
JobID: jobID,
@ -590,12 +570,19 @@ func insertProvisionerJobResource(ctx context.Context, db database.Store, jobID
Valid: true,
}
}
authToken := uuid.New()
if protoResource.Agent.GetToken() != "" {
authToken, err = uuid.Parse(protoResource.Agent.GetToken())
if err != nil {
return xerrors.Errorf("invalid auth token format; must be uuid: %w", err)
}
}
_, err := db.InsertProvisionerJobAgent(ctx, database.InsertProvisionerJobAgentParams{
_, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{
ID: resource.AgentID.UUID,
CreatedAt: database.Now(),
ResourceID: resource.ID,
AuthToken: uuid.New(),
AuthToken: authToken,
AuthInstanceID: instanceID,
EnvironmentVariables: env,
StartupScript: sql.NullString{

View File

@ -1,25 +0,0 @@
package coderd_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
)
func TestProvisionerDaemons(t *testing.T) {
// Tests for properly processing specific job types should be placed
// in their respective files.
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.NewProvisionerDaemon(t, client)
require.Eventually(t, func() bool {
daemons, err := client.ProvisionerDaemons(context.Background())
require.NoError(t, err)
return len(daemons) > 0
}, 3*time.Second, 50*time.Millisecond)
}

View File

@ -17,38 +17,29 @@ import (
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
"github.com/coder/coder/httpmw"
)
// ProvisionerJobStaus represents the at-time state of a job.
type ProvisionerJobStatus string
// Completed returns whether the job is still processing.
func (p ProvisionerJobStatus) Completed() bool {
return p == ProvisionerJobStatusSucceeded || p == ProvisionerJobStatusFailed || p == ProvisionerJobStatusCancelled
}
const (
ProvisionerJobStatusPending ProvisionerJobStatus = "pending"
ProvisionerJobStatusRunning ProvisionerJobStatus = "running"
ProvisionerJobStatusSucceeded ProvisionerJobStatus = "succeeded"
ProvisionerJobStatusCancelled ProvisionerJobStatus = "canceled"
ProvisionerJobStatusFailed ProvisionerJobStatus = "failed"
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"`
UpdatedAt time.Time `json:"updated_at"`
StartedAt *time.Time `json:"started_at,omitempty"`
CancelledAt *time.Time `json:"canceled_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
Status ProvisionerJobStatus `json:"status"`
Error string `json:"error,omitempty"`
Provisioner database.ProvisionerType `json:"provisioner"`
WorkerID *uuid.UUID `json:"worker_id,omitempty"`
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"`
}
// ProvisionerJobLog represents a single log from a provisioner job.
type ProvisionerJobLog struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
@ -57,31 +48,6 @@ type ProvisionerJobLog struct {
Output string `json:"output"`
}
type ProvisionerJobResource 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"`
}
type ProvisionerJobAgent 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"`
}
func (*api) provisionerJobByID(rw http.ResponseWriter, r *http.Request) {
job := httpmw.ProvisionerJobParam(r)
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertProvisionerJob(job))
}
// 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();
@ -89,7 +55,7 @@ func (*api) provisionerJobByID(rw http.ResponseWriter, r *http.Request) {
// 2. GET /logs?after=<timestamp>&follow
// The combination of these responses should provide all current logs
// to the consumer, and future logs are streamed in the follow request.
func (api *api) provisionerJobLogsByID(rw http.ResponseWriter, r *http.Request) {
func (api *api) provisionerJobLogs(rw http.ResponseWriter, r *http.Request, job database.ProvisionerJob) {
follow := r.URL.Query().Has("follow")
afterRaw := r.URL.Query().Get("after")
beforeRaw := r.URL.Query().Get("before")
@ -131,7 +97,6 @@ func (api *api) provisionerJobLogsByID(rw http.ResponseWriter, r *http.Request)
before = database.Now()
}
job := httpmw.ProvisionerJobParam(r)
if !follow {
logs, err := api.Database.GetProvisionerLogsByIDBetween(r.Context(), database.GetProvisionerLogsByIDBetweenParams{
JobID: job.ID,
@ -231,13 +196,56 @@ func (api *api) provisionerJobLogsByID(rw http.ResponseWriter, r *http.Request)
api.Logger.Warn(r.Context(), "streaming job logs; checking if completed", slog.Error(err), slog.F("job_id", job.ID.String()))
continue
}
if convertProvisionerJob(job).Status.Completed() {
if job.CompletedAt.Valid {
return
}
}
}
}
func (api *api) provisionerJobResources(rw http.ResponseWriter, r *http.Request, job database.ProvisionerJob) {
if !job.CompletedAt.Valid {
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: "Job hasn't completed!",
})
return
}
resources, err := api.Database.GetWorkspaceResourcesByJobID(r.Context(), job.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job resources: %s", err),
})
return
}
apiResources := make([]WorkspaceResource, 0)
for _, resource := range resources {
if !resource.AgentID.Valid {
apiResources = append(apiResources, convertWorkspaceResource(resource, nil))
continue
}
agent, err := api.Database.GetWorkspaceAgentByResourceID(r.Context(), resource.ID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job agent: %s", err),
})
return
}
apiAgent, err := convertWorkspaceAgent(agent)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("convert provisioner job agent: %s", err),
})
return
}
apiResources = append(apiResources, convertWorkspaceResource(resource, &apiAgent))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiResources)
}
func convertProvisionerJobLog(provisionerJobLog database.ProvisionerJobLog) ProvisionerJobLog {
return ProvisionerJobLog{
ID: provisionerJobLog.ID,
@ -250,19 +258,14 @@ func convertProvisionerJobLog(provisionerJobLog database.ProvisionerJobLog) Prov
func convertProvisionerJob(provisionerJob database.ProvisionerJob) ProvisionerJob {
job := ProvisionerJob{
ID: provisionerJob.ID,
CreatedAt: provisionerJob.CreatedAt,
UpdatedAt: provisionerJob.UpdatedAt,
Error: provisionerJob.Error.String,
Provisioner: provisionerJob.Provisioner,
ID: provisionerJob.ID,
CreatedAt: provisionerJob.CreatedAt,
Error: provisionerJob.Error.String,
}
// Applying values optional to the struct.
if provisionerJob.StartedAt.Valid {
job.StartedAt = &provisionerJob.StartedAt.Time
}
if provisionerJob.CancelledAt.Valid {
job.CancelledAt = &provisionerJob.CancelledAt.Time
}
if provisionerJob.CompletedAt.Valid {
job.CompletedAt = &provisionerJob.CompletedAt.Time
}
@ -272,20 +275,20 @@ func convertProvisionerJob(provisionerJob database.ProvisionerJob) ProvisionerJo
switch {
case provisionerJob.CancelledAt.Valid:
job.Status = ProvisionerJobStatusCancelled
job.Status = ProvisionerJobCancelled
case !provisionerJob.StartedAt.Valid:
job.Status = ProvisionerJobStatusPending
job.Status = ProvisionerJobPending
case provisionerJob.CompletedAt.Valid:
if job.Error == "" {
job.Status = ProvisionerJobStatusSucceeded
job.Status = ProvisionerJobSucceeded
} else {
job.Status = ProvisionerJobStatusFailed
job.Status = ProvisionerJobFailed
}
case database.Now().Sub(provisionerJob.UpdatedAt) > 30*time.Second:
job.Status = ProvisionerJobStatusFailed
job.Status = ProvisionerJobFailed
job.Error = "Worker failed to update job in time."
default:
job.Status = ProvisionerJobStatusRunning
job.Status = ProvisionerJobRunning
}
return job

View File

@ -2,7 +2,6 @@ package coderd_test
import (
"context"
"net/http"
"testing"
"time"
@ -10,239 +9,19 @@ import (
"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"
)
func TestPostProvisionerImportJobByOrganization(t *testing.T) {
func TestProvisionerJobLogs(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.NewProvisionerDaemon(t, client)
before := time.Now()
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{
Parse: []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{
ParameterSchemas: []*proto.ParameterSchema{},
},
},
}},
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "dev",
Type: "ec2_instance",
}},
},
},
}},
})
logs, err := client.ProjectImportJobLogsAfter(context.Background(), user.Organization, job.ID, before)
require.NoError(t, err)
for {
log, ok := <-logs
if !ok {
break
}
t.Log(log.Output)
}
})
t.Run("CreateWithParameters", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.NewProvisionerDaemon(t, client)
data, err := echo.Tar(&echo.Responses{
Parse: []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{
ParameterSchemas: []*proto.ParameterSchema{{
Name: "test",
RedisplayValue: true,
}},
},
},
}},
Provision: echo.ProvisionComplete,
})
require.NoError(t, err)
file, err := client.UploadFile(context.Background(), codersdk.ContentTypeTar, data)
require.NoError(t, err)
job, err := client.CreateProjectImportJob(context.Background(), user.Organization, coderd.CreateProjectImportJobRequest{
StorageSource: file.Hash,
StorageMethod: database.ProvisionerStorageMethodFile,
Provisioner: database.ProvisionerTypeEcho,
ParameterValues: []coderd.CreateParameterValueRequest{{
Name: "test",
SourceValue: "somevalue",
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
}},
})
require.NoError(t, err)
job = coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
values, err := client.ProjectImportJobParameters(context.Background(), user.Organization, job.ID)
require.NoError(t, err)
require.Equal(t, "somevalue", values[0].SourceValue)
})
}
func TestProvisionerJobParametersByID(t *testing.T) {
t.Parallel()
t.Run("NotImported", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
_, err := client.ProjectImportJobParameters(context.Background(), user.Organization, job.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{
Parse: []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{
ParameterSchemas: []*proto.ParameterSchema{{
Name: "example",
DefaultSource: &proto.ParameterSource{
Scheme: proto.ParameterSource_DATA,
Value: "hello",
},
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
},
}},
},
},
}},
Provision: echo.ProvisionComplete,
})
job = coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
params, err := client.ProjectImportJobParameters(context.Background(), user.Organization, job.ID)
require.NoError(t, err)
require.Len(t, params, 1)
})
t.Run("ListNoRedisplay", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{
Parse: []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{
ParameterSchemas: []*proto.ParameterSchema{{
Name: "example",
DefaultSource: &proto.ParameterSource{
Scheme: proto.ParameterSource_DATA,
Value: "tomato",
},
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
},
RedisplayValue: false,
}},
},
},
}},
Provision: echo.ProvisionComplete,
})
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
params, err := client.ProjectImportJobParameters(context.Background(), user.Organization, job.ID)
require.NoError(t, err)
require.Len(t, params, 1)
require.NotNil(t, params[0])
require.Equal(t, params[0].SourceValue, "")
})
}
func TestProvisionerJobResourcesByID(t *testing.T) {
t.Parallel()
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "hello",
Type: "ec2_instance",
}},
},
},
}},
})
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
resources, err := client.ProjectImportJobResources(context.Background(), user.Organization, job.ID)
require.NoError(t, err)
// One for start, and one for stop!
require.Len(t, resources, 2)
})
}
func TestProvisionerJobLogsByName(t *testing.T) {
t.Parallel()
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "log-output",
},
},
}, {
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
}},
})
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, history.ProvisionJobID)
// Return the log after completion!
logs, err := client.WorkspaceProvisionJobLogsBefore(context.Background(), user.Organization, history.ProvisionJobID, time.Time{})
require.NoError(t, err)
require.NotNil(t, logs)
require.Len(t, logs, 1)
})
t.Run("StreamAfterComplete", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
@ -257,18 +36,20 @@ func TestProvisionerJobLogsByName(t *testing.T) {
},
}},
})
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
before := time.Now().UTC()
history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, history.ProvisionJobID)
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
logs, err := client.WorkspaceProvisionJobLogsAfter(context.Background(), user.Organization, history.ProvisionJobID, before)
ctx, cancelFunc := context.WithCancel(context.Background())
t.Cleanup(cancelFunc)
logs, err := client.WorkspaceBuildLogsAfter(ctx, build.ID, before)
require.NoError(t, err)
log, ok := <-logs
require.True(t, ok)
@ -281,9 +62,9 @@ func TestProvisionerJobLogsByName(t *testing.T) {
t.Run("StreamWhileRunning", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
@ -298,21 +79,56 @@ func TestProvisionerJobLogsByName(t *testing.T) {
},
}},
})
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
before := database.Now()
history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
logs, err := client.WorkspaceProvisionJobLogsAfter(context.Background(), user.Organization, history.ProvisionJobID, before)
ctx, cancelFunc := context.WithCancel(context.Background())
t.Cleanup(cancelFunc)
logs, err := client.WorkspaceBuildLogsAfter(ctx, build.ID, before)
require.NoError(t, err)
log := <-logs
require.Equal(t, "log-output", log.Output)
// Make sure the channel automatically closes!
_, ok := <-logs
require.False(t, ok)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "log-output",
},
},
}, {
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
}},
})
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
logs, err := client.WorkspaceBuildLogsBefore(context.Background(), build.ID, time.Now())
require.NoError(t, err)
require.Len(t, logs, 1)
})
}

View File

@ -8,6 +8,7 @@ import (
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/google/uuid"
"golang.org/x/xerrors"
@ -27,21 +28,24 @@ type User struct {
Username string `json:"username" validate:"required"`
}
// CreateInitialUserRequest provides options to create the initial
// user for a Coder deployment. The organization provided will be
// created as well.
type CreateInitialUserRequest struct {
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"`
}
// CreateUserRequest provides options for creating a new user.
// 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"`
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.
@ -60,8 +64,18 @@ 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) user(rw http.ResponseWriter, r *http.Request) {
func (api *api) firstUser(rw http.ResponseWriter, r *http.Request) {
userCount, err := api.Database.GetUserCount(r.Context())
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@ -81,8 +95,8 @@ func (api *api) user(rw http.ResponseWriter, r *http.Request) {
}
// Creates the initial user for a Coder deployment.
func (api *api) postUser(rw http.ResponseWriter, r *http.Request) {
var createUser CreateInitialUserRequest
func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) {
var createUser CreateFirstUserRequest
if !httpapi.Read(rw, r, &createUser) {
return
}
@ -111,6 +125,7 @@ func (api *api) postUser(rw http.ResponseWriter, r *http.Request) {
// Create the user, organization, and membership to the user.
var user database.User
var organization database.Organization
err = api.Database.InTx(func(s database.Store) error {
user, err = api.Database.InsertUser(r.Context(), database.InsertUserParams{
ID: uuid.NewString(),
@ -124,7 +139,7 @@ func (api *api) postUser(rw http.ResponseWriter, r *http.Request) {
if err != nil {
return xerrors.Errorf("create user: %w", err)
}
organization, err := api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{
organization, err = api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{
ID: uuid.NewString(),
Name: createUser.Organization,
CreatedAt: database.Now(),
@ -153,11 +168,16 @@ func (api *api) postUser(rw http.ResponseWriter, r *http.Request) {
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertUser(user))
render.JSON(rw, r, CreateFirstUserResponse{
UserID: user.ID,
OrganizationID: organization.ID,
})
}
// Creates a new user.
func (api *api) postUsers(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
var createUser CreateUserRequest
if !httpapi.Read(rw, r, &createUser) {
return
@ -179,6 +199,37 @@ func (api *api) postUsers(rw http.ResponseWriter, r *http.Request) {
return
}
organization, err := api.Database.GetOrganizationByID(r.Context(), createUser.OrganizationID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: "organization does not exist with the provided id",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organization: %s", err),
})
return
}
// Check if the caller has permissions to the organization requested.
_, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
OrganizationID: organization.ID,
UserID: apiKey.UserID,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "you are not authorized to add members to that organization",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organization member: %s", err),
})
return
}
hashedPassword, err := userpassword.Hash(createUser.Password)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@ -187,18 +238,35 @@ func (api *api) postUsers(rw http.ResponseWriter, r *http.Request) {
return
}
user, err := api.Database.InsertUser(r.Context(), database.InsertUserParams{
ID: uuid.NewString(),
Email: createUser.Email,
HashedPassword: []byte(hashedPassword),
Username: createUser.Username,
LoginType: database.LoginTypeBuiltIn,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
var user database.User
err = api.Database.InTx(func(db database.Store) error {
user, err = db.InsertUser(r.Context(), database.InsertUserParams{
ID: uuid.NewString(),
Email: createUser.Email,
HashedPassword: []byte(hashedPassword),
Username: createUser.Username,
LoginType: database.LoginTypeBuiltIn,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create user: %w", err)
}
_, err = db.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
OrganizationID: organization.ID,
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Roles: []string{},
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
}
return nil
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("create user: %s", err.Error()),
Message: err.Error(),
})
return
}
@ -240,6 +308,97 @@ func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) {
render.JSON(rw, r, publicOrganizations)
}
func (api *api) organizationByUserAndName(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
organizationName := chi.URLParam(r, "organizationname")
organization, err := api.Database.GetOrganizationByName(r.Context(), organizationName)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("no organization found by name %q", organizationName),
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organization by name: %s", err),
})
return
}
_, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
OrganizationID: organization.ID,
UserID: user.ID,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "you are not a member of that organization",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organization member: %s", err),
})
return
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertOrganization(organization))
}
func (api *api) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
var req CreateOrganizationRequest
if !httpapi.Read(rw, r, &req) {
return
}
_, err := api.Database.GetOrganizationByName(r.Context(), req.Name)
if err == nil {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: "organization already exists with that name",
})
return
}
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organization: %s", err.Error()),
})
return
}
var organization database.Organization
err = api.Database.InTx(func(db database.Store) error {
organization, err = api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{
ID: uuid.NewString(),
Name: req.Name,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create organization: %w", err)
}
_, err = api.Database.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
OrganizationID: organization.ID,
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Roles: []string{"organization-admin"},
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
}
return nil
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertOrganization(organization))
}
// Authenticates the user with an email and password.
func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) {
var loginWithPassword LoginWithPasswordRequest
@ -318,7 +477,7 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) {
}
// Creates a new session key, used for logging in via the CLI
func (api *api) postKeyForUser(rw http.ResponseWriter, r *http.Request) {
func (api *api) postAPIKey(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
apiKey := httpmw.APIKey(r)
@ -375,6 +534,141 @@ func (*api) postLogout(rw http.ResponseWriter, r *http.Request) {
render.Status(r, http.StatusOK)
}
// Create a new workspace for the currently authenticated user.
func (api *api) postWorkspacesByUser(rw http.ResponseWriter, r *http.Request) {
var createWorkspace CreateWorkspaceRequest
if !httpapi.Read(rw, r, &createWorkspace) {
return
}
apiKey := httpmw.APIKey(r)
project, err := api.Database.GetProjectByID(r.Context(), createWorkspace.ProjectID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("project %q doesn't exist", createWorkspace.ProjectID.String()),
Errors: []httpapi.Error{{
Field: "project_id",
Code: "not_found",
}},
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project: %s", err),
})
return
}
_, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
OrganizationID: project.OrganizationID,
UserID: apiKey.UserID,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "you aren't allowed to access projects in that organization",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organization member: %s", err),
})
return
}
workspace, err := api.Database.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{
OwnerID: apiKey.UserID,
Name: createWorkspace.Name,
})
if err == nil {
// If the workspace already exists, don't allow creation.
project, err := api.Database.GetProjectByID(r.Context(), workspace.ProjectID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("find project for conflicting workspace name %q: %s", createWorkspace.Name, err),
})
return
}
// The project is fetched for clarity to the user on where the conflicting name may be.
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: fmt.Sprintf("workspace %q already exists in the %q project", createWorkspace.Name, project.Name),
Errors: []httpapi.Error{{
Field: "name",
Code: "exists",
}},
})
return
}
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace by name: %s", err.Error()),
})
return
}
// Workspaces are created without any versions.
workspace, err = api.Database.InsertWorkspace(r.Context(), database.InsertWorkspaceParams{
ID: uuid.New(),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
OwnerID: apiKey.UserID,
ProjectID: project.ID,
Name: createWorkspace.Name,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert workspace: %s", err),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertWorkspace(workspace))
}
func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
workspaces, err := api.Database.GetWorkspacesByUserID(r.Context(), user.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspaces: %s", err),
})
return
}
apiWorkspaces := make([]Workspace, 0, len(workspaces))
for _, workspace := range workspaces {
apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiWorkspaces)
}
func (api *api) workspaceByUserAndName(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
workspaceName := chi.URLParam(r, "workspacename")
workspace, err := api.Database.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{
OwnerID: user.ID,
Name: workspaceName,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("no workspace found by name %q", workspaceName),
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace by name: %s", err),
})
return
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertWorkspace(workspace))
}
// Generates a new ID and secret for an API key.
func generateAPIKeyIDSecret() (id string, secret string, err error) {
// Length of an API Key ID.

View File

@ -5,6 +5,7 @@ import (
"net/http"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd"
@ -13,40 +14,20 @@ import (
"github.com/coder/coder/httpmw"
)
func TestUser(t *testing.T) {
t.Parallel()
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
has, err := client.HasInitialUser(context.Background())
require.NoError(t, err)
require.False(t, has)
})
t.Run("Found", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
has, err := client.HasInitialUser(context.Background())
require.NoError(t, err)
require.True(t, has)
})
}
func TestPostUser(t *testing.T) {
func TestFirstUser(t *testing.T) {
t.Parallel()
t.Run("BadRequest", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{})
_, err := client.CreateFirstUser(context.Background(), coderd.CreateFirstUserRequest{})
require.Error(t, err)
})
t.Run("AlreadyExists", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{
_ = coderdtest.CreateFirstUser(t, client)
_, err := client.CreateFirstUser(context.Background(), coderd.CreateFirstUserRequest{
Email: "some@email.com",
Username: "exampleuser",
Password: "password",
@ -60,89 +41,7 @@ func TestPostUser(t *testing.T) {
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
})
}
func TestPostUsers(t *testing.T) {
t.Parallel()
t.Run("BadRequest", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{})
require.Error(t, err)
})
t.Run("Conflicting", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
_, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{
Email: user.Email,
Username: user.Username,
Password: "password",
Organization: "someorg",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: "another@user.org",
Username: "someone-else",
Password: "testing",
})
require.NoError(t, err)
})
}
func TestUserByName(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.User(context.Background(), "")
require.NoError(t, err)
}
func TestOrganizationsByUser(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
orgs, err := client.UserOrganizations(context.Background(), "")
require.NoError(t, err)
require.NotNil(t, orgs)
require.Len(t, orgs, 1)
}
func TestPostKey(t *testing.T) {
t.Parallel()
t.Run("InvalidUser", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
// Clear session token
client.SessionToken = ""
// ...and request an API key
_, err := client.CreateAPIKey(context.Background())
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
})
t.Run("Success", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
apiKey, err := client.CreateAPIKey(context.Background())
require.NotNil(t, apiKey)
require.GreaterOrEqual(t, len(apiKey.Key), 2)
require.NoError(t, err)
_ = coderdtest.CreateFirstUser(t, client)
})
}
@ -163,9 +62,16 @@ func TestPostLogin(t *testing.T) {
t.Run("BadPassword", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
_, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: user.Email,
req := coderd.CreateFirstUserRequest{
Email: "testuser@coder.com",
Username: "testuser",
Password: "testpass",
Organization: "testorg",
}
_, err := client.CreateFirstUser(context.Background(), req)
require.NoError(t, err)
_, err = client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: req.Email,
Password: "badpass",
})
var apiErr *codersdk.Error
@ -176,10 +82,17 @@ func TestPostLogin(t *testing.T) {
t.Run("Success", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
_, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: user.Email,
Password: user.Password,
req := coderd.CreateFirstUserRequest{
Email: "testuser@coder.com",
Username: "testuser",
Password: "testpass",
Organization: "testorg",
}
_, err := client.CreateFirstUser(context.Background(), req)
require.NoError(t, err)
_, err = client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: req.Email,
Password: req.Password,
})
require.NoError(t, err)
})
@ -192,7 +105,7 @@ func TestPostLogout(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
fullURL, err := client.URL.Parse("/api/v2/logout")
fullURL, err := client.URL.Parse("/api/v2/users/logout")
require.NoError(t, err, "Server URL should parse successfully")
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, fullURL.String(), nil)
@ -211,3 +124,298 @@ func TestPostLogout(t *testing.T) {
require.Equal(t, cookies[0].MaxAge, -1, "Cookie should be set to delete")
})
}
func TestPostUsers(t *testing.T) {
t.Parallel()
t.Run("NoAuth", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{})
require.Error(t, err)
})
t.Run("Conflicting", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
me, err := client.User(context.Background(), "")
require.NoError(t, err)
_, err = client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: me.Email,
Username: me.Username,
Password: "password",
OrganizationID: "someorg",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
t.Run("OrganizationNotFound", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
_, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{
OrganizationID: "not-exists",
Email: "another@user.org",
Username: "someone-else",
Password: "testing",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("OrganizationNoAccess", func(t *testing.T) {
t.Parallel()
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{
Name: "another",
})
require.NoError(t, err)
_, err = client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: "some@domain.com",
Username: "anotheruser",
Password: "testing",
OrganizationID: org.ID,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{
OrganizationID: user.OrganizationID,
Email: "another@user.org",
Username: "someone-else",
Password: "testing",
})
require.NoError(t, err)
})
}
func TestUserByName(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
_, err := client.User(context.Background(), "")
require.NoError(t, err)
}
func TestOrganizationsByUser(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
orgs, err := client.OrganizationsByUser(context.Background(), "")
require.NoError(t, err)
require.NotNil(t, orgs)
require.Len(t, orgs, 1)
}
func TestOrganizationByUserAndName(t *testing.T) {
t.Parallel()
t.Run("NoExist", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
_, err := client.OrganizationByName(context.Background(), "", "nothing")
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("NoMember", func(t *testing.T) {
t.Parallel()
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{
Name: "another",
})
require.NoError(t, err)
_, err = client.OrganizationByName(context.Background(), "", org.Name)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
})
t.Run("Valid", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
org, err := client.Organization(context.Background(), user.OrganizationID)
require.NoError(t, err)
_, err = client.OrganizationByName(context.Background(), "", org.Name)
require.NoError(t, err)
})
}
func TestPostOrganizationsByUser(t *testing.T) {
t.Parallel()
t.Run("Conflict", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
org, err := client.Organization(context.Background(), user.OrganizationID)
require.NoError(t, err)
_, err = client.CreateOrganization(context.Background(), "", coderd.CreateOrganizationRequest{
Name: org.Name,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
_, err := client.CreateOrganization(context.Background(), "", coderd.CreateOrganizationRequest{
Name: "new",
})
require.NoError(t, err)
})
}
func TestPostAPIKey(t *testing.T) {
t.Parallel()
t.Run("InvalidUser", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
client.SessionToken = ""
_, err := client.CreateAPIKey(context.Background(), "")
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
})
t.Run("Success", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
apiKey, err := client.CreateAPIKey(context.Background(), "")
require.NotNil(t, apiKey)
require.GreaterOrEqual(t, len(apiKey.Key), 2)
require.NoError(t, err)
})
}
func TestPostWorkspacesByUser(t *testing.T) {
t.Parallel()
t.Run("InvalidProject", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
_, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
ProjectID: uuid.New(),
Name: "workspace",
})
require.Error(t, err)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
})
t.Run("NoProjectAccess", func(t *testing.T) {
t.Parallel()
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{
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{
ProjectID: project.ID,
Name: "workspace",
})
require.Error(t, err)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
})
t.Run("AlreadyExists", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
ProjectID: project.ID,
Name: workspace.Name,
})
require.Error(t, err)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID)
_ = coderdtest.CreateWorkspace(t, client, "", project.ID)
})
}
func TestWorkspacesByUser(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
_, err := client.WorkspacesByUser(context.Background(), "")
require.NoError(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID)
_ = coderdtest.CreateWorkspace(t, client, "", project.ID)
workspaces, err := client.WorkspacesByUser(context.Background(), "")
require.NoError(t, err)
require.Len(t, workspaces, 1)
})
}
func TestWorkspaceByUserAndName(t *testing.T) {
t.Parallel()
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
_, err := client.WorkspaceByName(context.Background(), "", "something")
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.WorkspaceByName(context.Background(), "", workspace.Name)
require.NoError(t, err)
})
}

96
coderd/workspacebuilds.go Normal file
View File

@ -0,0 +1,96 @@
package coderd
import (
"fmt"
"net/http"
"time"
"github.com/go-chi/render"
"github.com/google/uuid"
"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)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job)))
}
func (api *api) workspaceBuildResources(rw http.ResponseWriter, r *http.Request) {
workspaceBuild := httpmw.WorkspaceBuildParam(r)
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
api.provisionerJobResources(rw, r, job)
}
func (api *api) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) {
workspaceBuild := httpmw.WorkspaceBuildParam(r)
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
api.provisionerJobLogs(rw, r, job)
}
func convertWorkspaceBuild(workspaceBuild database.WorkspaceBuild, job ProvisionerJob) WorkspaceBuild {
//nolint:unconvert
return WorkspaceBuild(WorkspaceBuild{
ID: workspaceBuild.ID,
CreatedAt: workspaceBuild.CreatedAt,
UpdatedAt: workspaceBuild.UpdatedAt,
WorkspaceID: workspaceBuild.WorkspaceID,
ProjectVersionID: workspaceBuild.ProjectVersionID,
BeforeID: workspaceBuild.BeforeID.UUID,
AfterID: workspaceBuild.AfterID.UUID,
Name: workspaceBuild.Name,
Transition: workspaceBuild.Transition,
Initiator: workspaceBuild.Initiator,
Job: job,
})
}
func convertWorkspaceResource(resource database.WorkspaceResource, agent *WorkspaceAgent) WorkspaceResource {
return WorkspaceResource{
ID: resource.ID,
CreatedAt: resource.CreatedAt,
JobID: resource.JobID,
Transition: resource.Transition,
Type: resource.Type,
Name: resource.Name,
Agent: agent,
}
}

View File

@ -0,0 +1,150 @@
package coderd_test
import (
"context"
"net/http"
"testing"
"time"
"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"
)
func TestWorkspaceBuild(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
_, err = client.WorkspaceBuild(context.Background(), build.ID)
require.NoError(t, err)
}
func TestWorkspaceBuildResources(t *testing.T) {
t.Parallel()
t.Run("ListRunning", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
closeDaemon := coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
closeDaemon.Close()
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
_, err = client.WorkspaceResourcesByBuild(context.Background(), build.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
Agent: &proto.Agent{
Id: "something",
Auth: &proto.Agent_Token{},
},
}, {
Name: "another",
Type: "example",
}},
},
},
}},
})
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
resources, err := client.WorkspaceResourcesByBuild(context.Background(), build.ID)
require.NoError(t, err)
require.NotNil(t, resources)
require.Len(t, resources, 2)
require.Equal(t, "some", resources[0].Name)
require.Equal(t, "example", resources[0].Type)
require.NotNil(t, resources[0].Agent)
})
}
func TestWorkspaceBuildLogs(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
before := time.Now()
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "example",
},
},
}, {
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
Agent: &proto.Agent{
Id: "something",
Auth: &proto.Agent_Token{},
},
}, {
Name: "another",
Type: "example",
}},
},
},
}},
})
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
ctx, cancelFunc := context.WithCancel(context.Background())
t.Cleanup(cancelFunc)
logs, err := client.WorkspaceBuildLogsAfter(ctx, build.ID, before)
require.NoError(t, err)
log := <-logs
require.Equal(t, "example", log.Output)
}

View File

@ -1,244 +0,0 @@
package coderd
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"golang.org/x/xerrors"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
"github.com/coder/coder/httpmw"
)
// WorkspaceHistory is an at-point representation of a workspace state.
// Iterate on before/after to determine a chronological history.
type WorkspaceHistory 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"`
ProvisionJobID uuid.UUID `json:"provision_job_id"`
}
// CreateWorkspaceHistoryRequest provides options to update the latest workspace history.
type CreateWorkspaceHistoryRequest struct {
ProjectVersionID uuid.UUID `json:"project_version_id" validate:"required"`
Transition database.WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"`
}
func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Request) {
var createBuild CreateWorkspaceHistoryRequest
if !httpapi.Read(rw, r, &createBuild) {
return
}
user := httpmw.UserParam(r)
workspace := httpmw.WorkspaceParam(r)
projectVersion, err := api.Database.GetProjectVersionByID(r.Context(), createBuild.ProjectVersionID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "project version not found",
Errors: []httpapi.Error{{
Field: "project_version_id",
Code: "exists",
}},
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project version: %s", err),
})
return
}
projectVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.ImportJobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
projectVersionJobStatus := convertProvisionerJob(projectVersionJob).Status
switch projectVersionJobStatus {
case ProvisionerJobStatusPending, ProvisionerJobStatusRunning:
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 ProvisionerJobStatusFailed:
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 ProvisionerJobStatusCancelled:
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: "The provided project version was canceled during import. You cannot create workspaces using it!",
})
return
}
project, err := api.Database.GetProjectByID(r.Context(), projectVersion.ProjectID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project: %s", err),
})
return
}
// Store prior history ID if it exists to update it after we create new!
priorHistoryID := uuid.NullUUID{}
priorHistory, err := api.Database.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
if err == nil {
priorJob, err := api.Database.GetProvisionerJobByID(r.Context(), priorHistory.ProvisionJobID)
if err == nil && !convertProvisionerJob(priorJob).Status.Completed() {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: "a workspace build is already active",
})
return
}
priorHistoryID = uuid.NullUUID{
UUID: priorHistory.ID,
Valid: true,
}
} else if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get prior workspace history: %s", err),
})
return
}
var workspaceHistory database.WorkspaceHistory
// This must happen in a transaction to ensure history can be inserted, and
// the prior history can update it's "after" column to point at the new.
err = api.Database.InTx(func(db database.Store) error {
provisionerJobID := uuid.New()
workspaceHistory, err = db.InsertWorkspaceHistory(r.Context(), database.InsertWorkspaceHistoryParams{
ID: uuid.New(),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
WorkspaceID: workspace.ID,
ProjectVersionID: projectVersion.ID,
BeforeID: priorHistoryID,
Name: namesgenerator.GetRandomName(1),
Initiator: user.ID,
Transition: createBuild.Transition,
ProvisionJobID: provisionerJobID,
})
if err != nil {
return xerrors.Errorf("insert workspace history: %w", err)
}
input, err := json.Marshal(workspaceProvisionJob{
WorkspaceHistoryID: workspaceHistory.ID,
})
if err != nil {
return xerrors.Errorf("marshal provision job: %w", err)
}
_, err = db.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{
ID: provisionerJobID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
InitiatorID: user.ID,
OrganizationID: project.OrganizationID,
Provisioner: project.Provisioner,
Type: database.ProvisionerJobTypeWorkspaceProvision,
StorageMethod: projectVersionJob.StorageMethod,
StorageSource: projectVersionJob.StorageSource,
Input: input,
})
if err != nil {
return xerrors.Errorf("insert provisioner job: %w", err)
}
if priorHistoryID.Valid {
// Update the prior history entries "after" column.
err = db.UpdateWorkspaceHistoryByID(r.Context(), database.UpdateWorkspaceHistoryByIDParams{
ID: priorHistory.ID,
ProvisionerState: priorHistory.ProvisionerState,
UpdatedAt: database.Now(),
AfterID: uuid.NullUUID{
UUID: workspaceHistory.ID,
Valid: true,
},
})
if err != nil {
return xerrors.Errorf("update prior workspace history: %w", err)
}
}
return nil
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertWorkspaceHistory(workspaceHistory))
}
// Returns all workspace history. This is not sorted. Use before/after to chronologically sort.
func (api *api) workspaceHistoryByUser(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)
history, err := api.Database.GetWorkspaceHistoryByWorkspaceID(r.Context(), workspace.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
history = []database.WorkspaceHistory{}
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace history: %s", err),
})
return
}
apiHistory := make([]WorkspaceHistory, 0, len(history))
for _, history := range history {
apiHistory = append(apiHistory, convertWorkspaceHistory(history))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiHistory)
}
func (*api) workspaceHistoryByName(rw http.ResponseWriter, r *http.Request) {
workspaceHistory := httpmw.WorkspaceHistoryParam(r)
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertWorkspaceHistory(workspaceHistory))
}
// Converts the internal history representation to a public external-facing model.
func convertWorkspaceHistory(workspaceHistory database.WorkspaceHistory) WorkspaceHistory {
//nolint:unconvert
return WorkspaceHistory(WorkspaceHistory{
ID: workspaceHistory.ID,
CreatedAt: workspaceHistory.CreatedAt,
UpdatedAt: workspaceHistory.UpdatedAt,
WorkspaceID: workspaceHistory.WorkspaceID,
ProjectVersionID: workspaceHistory.ProjectVersionID,
BeforeID: workspaceHistory.BeforeID.UUID,
AfterID: workspaceHistory.AfterID.UUID,
Name: workspaceHistory.Name,
Transition: workspaceHistory.Transition,
Initiator: workspaceHistory.Initiator,
ProvisionJobID: workspaceHistory.ProvisionJobID,
})
}

View File

@ -1,166 +0,0 @@
package coderd_test
import (
"context"
"net/http"
"testing"
"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"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
)
func TestPostWorkspaceHistoryByUser(t *testing.T) {
t.Parallel()
t.Run("NoProjectVersion", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
_, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: uuid.New(),
Transition: database.WorkspaceTransitionStart,
})
require.Error(t, err)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
})
t.Run("ProjectVersionFailedImport", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{
Provision: []*proto.Provision_Response{{}},
})
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
_, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.Error(t, err)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
})
t.Run("AlreadyActive", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
closeDaemon := coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
// Close here so workspace history doesn't process!
closeDaemon.Close()
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
_, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
_, err = client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.Error(t, err)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
t.Run("UpdatePriorAfterField", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
firstHistory, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceProvisionJob(t, client, user.Organization, firstHistory.ProvisionJobID)
secondHistory, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
require.Equal(t, firstHistory.ID.String(), secondHistory.BeforeID.String())
firstHistory, err = client.WorkspaceHistory(context.Background(), "", workspace.Name, firstHistory.Name)
require.NoError(t, err)
require.Equal(t, secondHistory.ID.String(), firstHistory.AfterID.String())
})
}
func TestWorkspaceHistoryByUser(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
history, err := client.ListWorkspaceHistory(context.Background(), "me", workspace.Name)
require.NoError(t, err)
require.NotNil(t, history)
require.Len(t, history, 0)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
_, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
history, err := client.ListWorkspaceHistory(context.Background(), "me", workspace.Name)
require.NoError(t, err)
require.NotNil(t, history)
require.Len(t, history, 1)
})
}
func TestWorkspaceHistoryByName(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
_, err = client.WorkspaceHistory(context.Background(), "me", workspace.Name, history.Name)
require.NoError(t, err)
}

View File

@ -28,7 +28,7 @@ type WorkspaceAgentAuthenticateResponse struct {
// 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) postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
func (api *api) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
var req GoogleInstanceIdentityToken
if !httpapi.Read(rw, r, &req) {
return
@ -56,7 +56,7 @@ func (api *api) postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity(rw htt
})
return
}
agent, err := api.Database.GetProvisionerJobAgentByInstanceID(r.Context(), claims.Google.ComputeEngine.InstanceID)
agent, err := api.Database.GetWorkspaceAgentByInstanceID(r.Context(), claims.Google.ComputeEngine.InstanceID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("instance with id %q not found", claims.Google.ComputeEngine.InstanceID),
@ -69,7 +69,7 @@ func (api *api) postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity(rw htt
})
return
}
resource, err := api.Database.GetProvisionerJobResourceByID(r.Context(), agent.ResourceID)
resource, err := api.Database.GetWorkspaceResourceByID(r.Context(), agent.ResourceID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job resource: %s", err),
@ -83,7 +83,7 @@ func (api *api) postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity(rw htt
})
return
}
if job.Type != database.ProvisionerJobTypeWorkspaceProvision {
if job.Type != database.ProvisionerJobTypeWorkspaceBuild {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("%q jobs cannot be authenticated", job.Type),
})
@ -97,20 +97,20 @@ func (api *api) postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity(rw htt
})
return
}
resourceHistory, err := api.Database.GetWorkspaceHistoryByID(r.Context(), jobData.WorkspaceHistoryID)
resourceHistory, err := api.Database.GetWorkspaceBuildByID(r.Context(), jobData.WorkspaceBuildID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace history: %s", err),
Message: fmt.Sprintf("get workspace build: %s", err),
})
return
}
// This token should only be exchanged if the instance ID is valid
// for the latest history. If an instance ID is recycled by a cloud,
// we'd hate to leak access to a user's workspace.
latestHistory, err := api.Database.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), resourceHistory.WorkspaceID)
latestHistory, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), resourceHistory.WorkspaceID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get latest workspace history: %s", err),
Message: fmt.Sprintf("get latest workspace build: %s", err),
})
return
}

View File

@ -28,7 +28,7 @@ import (
"github.com/coder/coder/provisionersdk/proto"
)
func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) {
func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) {
t.Parallel()
t.Run("Expired", func(t *testing.T) {
t.Parallel()
@ -38,7 +38,7 @@ func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{
GoogleTokenValidator: validator,
})
_, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey))
_, err := client.AuthWorkspaceGoogleInstanceIdentity(context.Background(), "", createMetadataClient(signedKey))
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
@ -52,7 +52,7 @@ func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{
GoogleTokenValidator: validator,
})
_, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey))
_, err := client.AuthWorkspaceGoogleInstanceIdentity(context.Background(), "", createMetadataClient(signedKey))
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
@ -66,9 +66,9 @@ func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{
GoogleTokenValidator: validator,
})
user := coderdtest.CreateInitialUser(t, client)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
@ -88,17 +88,17 @@ func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) {
},
}},
})
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
firstHistory, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceProvisionJob(t, client, user.Organization, firstHistory.ProvisionJobID)
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
_, err = client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey))
_, err = client.AuthWorkspaceGoogleInstanceIdentity(context.Background(), "", createMetadataClient(signedKey))
require.NoError(t, err)
})
}

View File

@ -0,0 +1,236 @@
package coderd
import (
"database/sql"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/hashicorp/yamux"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
"github.com/coder/coder/httpmw"
"github.com/coder/coder/peerbroker"
"github.com/coder/coder/peerbroker/proto"
"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)
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
if !job.CompletedAt.Valid {
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: "Job hasn't completed!",
})
return
}
var apiAgent *WorkspaceAgent
if workspaceResource.AgentID.Valid {
agent, err := api.Database.GetWorkspaceAgentByResourceID(r.Context(), workspaceResource.ID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job agent: %s", err),
})
return
}
convertedAgent, err := convertWorkspaceAgent(agent)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("convert provisioner job agent: %s", err),
})
return
}
apiAgent = &convertedAgent
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertWorkspaceResource(workspaceResource, apiAgent))
}
func (api *api) workspaceResourceDial(rw http.ResponseWriter, r *http.Request) {
api.websocketWaitGroup.Add(1)
defer api.websocketWaitGroup.Done()
resource := httpmw.WorkspaceResourceParam(r)
if !resource.AgentID.Valid {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "resource doesn't have an agent",
})
return
}
agent, err := api.Database.GetWorkspaceAgentByResourceID(r.Context(), resource.ID)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("get provisioner job agent: %s", err),
})
return
}
conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("accept websocket: %s", err),
})
return
}
defer func() {
_ = conn.Close(websocket.StatusNormalClosure, "")
}()
config := yamux.DefaultConfig()
config.LogOutput = io.Discard
session, err := yamux.Server(websocket.NetConn(r.Context(), conn, websocket.MessageBinary), config)
if err != nil {
_ = conn.Close(websocket.StatusAbnormalClosure, err.Error())
return
}
err = peerbroker.ProxyListen(r.Context(), session, peerbroker.ProxyOptions{
ChannelID: agent.ID.String(),
Logger: api.Logger.Named("peerbroker-proxy-dial"),
Pubsub: api.Pubsub,
})
if err != nil {
_ = conn.Close(websocket.StatusInternalError, fmt.Sprintf("serve: %s", err))
return
}
}
func (api *api) workspaceAgentListen(rw http.ResponseWriter, r *http.Request) {
api.websocketWaitGroup.Add(1)
defer api.websocketWaitGroup.Done()
agent := httpmw.WorkspaceAgent(r)
conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("accept websocket: %s", err),
})
return
}
defer func() {
_ = conn.Close(websocket.StatusNormalClosure, "")
}()
config := yamux.DefaultConfig()
config.LogOutput = io.Discard
session, err := yamux.Server(websocket.NetConn(r.Context(), conn, websocket.MessageBinary), config)
if err != nil {
_ = conn.Close(websocket.StatusAbnormalClosure, err.Error())
return
}
closer, err := peerbroker.ProxyDial(proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(session)), peerbroker.ProxyOptions{
ChannelID: agent.ID.String(),
Pubsub: api.Pubsub,
Logger: api.Logger.Named("peerbroker-proxy-listen"),
})
if err != nil {
_ = conn.Close(websocket.StatusAbnormalClosure, err.Error())
return
}
defer closer.Close()
err = api.Database.UpdateWorkspaceAgentByID(r.Context(), database.UpdateWorkspaceAgentByIDParams{
ID: agent.ID,
UpdatedAt: sql.NullTime{
Time: database.Now(),
Valid: true,
},
})
if err != nil {
_ = conn.Close(websocket.StatusAbnormalClosure, err.Error())
return
}
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-session.CloseChan():
return
case <-ticker.C:
err = api.Database.UpdateWorkspaceAgentByID(r.Context(), database.UpdateWorkspaceAgentByIDParams{
ID: agent.ID,
UpdatedAt: sql.NullTime{
Time: database.Now(),
Valid: true,
},
})
if err != nil {
_ = conn.Close(websocket.StatusAbnormalClosure, err.Error())
return
}
}
}
}
func convertWorkspaceAgent(agent database.WorkspaceAgent) (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 WorkspaceAgent{
ID: agent.ID,
CreatedAt: agent.CreatedAt,
UpdatedAt: agent.UpdatedAt.Time,
ResourceID: agent.ResourceID,
InstanceID: agent.AuthInstanceID.String,
StartupScript: agent.StartupScript.String,
EnvironmentVariables: envs,
}, nil
}

View File

@ -0,0 +1,126 @@
package coderd_test
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/agent"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/database"
"github.com/coder/coder/peer"
"github.com/coder/coder/peerbroker"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
)
func TestWorkspaceResource(t *testing.T) {
t.Parallel()
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
Agent: &proto.Agent{
Id: "something",
Auth: &proto.Agent_Token{},
},
}},
},
},
}},
})
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
resources, err := client.WorkspaceResourcesByBuild(context.Background(), build.ID)
require.NoError(t, err)
_, err = client.WorkspaceResource(context.Background(), resources[0].ID)
require.NoError(t, err)
})
}
func TestWorkspaceAgentListen(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
daemonCloser := coderdtest.NewProvisionerDaemon(t, client)
authToken := uuid.NewString()
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionDryRun: echo.ProvisionComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
Agent: &proto.Agent{
Id: uuid.NewString(),
Auth: &proto.Agent_Token{
Token: authToken,
},
},
}},
},
},
}},
})
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
daemonCloser.Close()
agentClient := codersdk.New(client.URL)
agentClient.SessionToken = authToken
agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &peer.ConnOptions{
Logger: slogtest.Make(t, nil),
})
t.Cleanup(func() {
_ = agentCloser.Close()
})
resources := coderdtest.AwaitWorkspaceAgents(t, client, build.ID)
workspaceClient, err := client.DialWorkspaceAgent(context.Background(), resources[0].ID)
require.NoError(t, err)
t.Cleanup(func() {
_ = workspaceClient.DRPCConn().Close()
})
stream, err := workspaceClient.NegotiateConnection(context.Background())
require.NoError(t, err)
conn, err := peerbroker.Dial(stream, nil, &peer.ConnOptions{
Logger: slogtest.Make(t, nil).Named("client").Leveled(slog.LevelDebug),
})
require.NoError(t, err)
t.Cleanup(func() {
_ = conn.Close()
})
_, err = conn.Ping()
require.NoError(t, err)
}

View File

@ -2,12 +2,16 @@ package coderd
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"golang.org/x/xerrors"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
@ -18,160 +22,268 @@ import (
// project versions, and can be updated.
type Workspace database.Workspace
// 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"`
// 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"`
}
// Returns all workspaces across all projects and organizations.
func (api *api) workspaces(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
workspaces, err := api.Database.GetWorkspacesByUserID(r.Context(), apiKey.UserID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
func (*api) workspace(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertWorkspace(workspace))
}
func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)
builds, err := api.Database.GetWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
if err != nil {
return
}
jobIDs := make([]uuid.UUID, 0, len(builds))
for _, version := range builds {
jobIDs = append(jobIDs, version.JobID)
}
jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspaces: %s", err),
Message: fmt.Sprintf("get jobs: %s", err),
})
return
}
apiWorkspaces := make([]Workspace, 0, len(workspaces))
for _, workspace := range workspaces {
apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace))
jobByID := map[string]database.ProvisionerJob{}
for _, job := range jobs {
jobByID[job.ID.String()] = job
}
apiBuilds := make([]WorkspaceBuild, 0)
for _, build := range builds {
job, exists := jobByID[build.JobID.String()]
if !exists {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("job %q doesn't exist for build %q", build.JobID, build.ID),
})
return
}
apiBuilds = append(apiBuilds, convertWorkspaceBuild(build, convertProvisionerJob(job)))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiWorkspaces)
render.JSON(rw, r, apiBuilds)
}
// Create a new workspace for the currently authenticated user.
func (api *api) postWorkspaceByUser(rw http.ResponseWriter, r *http.Request) {
var createWorkspace CreateWorkspaceRequest
if !httpapi.Read(rw, r, &createWorkspace) {
func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
workspace := httpmw.WorkspaceParam(r)
var createBuild CreateWorkspaceBuildRequest
if !httpapi.Read(rw, r, &createBuild) {
return
}
apiKey := httpmw.APIKey(r)
project, err := api.Database.GetProjectByID(r.Context(), createWorkspace.ProjectID)
projectVersion, err := api.Database.GetProjectVersionByID(r.Context(), createBuild.ProjectVersionID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("project %q doesn't exist", createWorkspace.ProjectID.String()),
Message: "project version not found",
Errors: []httpapi.Error{{
Field: "project_id",
Code: "not_found",
Field: "project_version_id",
Code: "exists",
}},
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project version: %s", err),
})
return
}
projectVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
projectVersionJobStatus := convertProvisionerJob(projectVersionJob).Status
switch projectVersionJobStatus {
case ProvisionerJobPending, 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:
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:
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: "The provided project version was canceled during import. You cannot create workspaces using it!",
})
return
}
project, err := api.Database.GetProjectByID(r.Context(), projectVersion.ProjectID.UUID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project: %s", err),
})
return
}
_, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
OrganizationID: project.OrganizationID,
UserID: apiKey.UserID,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "you aren't allowed to access projects in that organization",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organization member: %s", err),
})
return
}
workspace, err := api.Database.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{
OwnerID: apiKey.UserID,
Name: createWorkspace.Name,
})
// Store prior history ID if it exists to update it after we create new!
priorHistoryID := uuid.NullUUID{}
priorHistory, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
if err == nil {
// If the workspace already exists, don't allow creation.
project, err := api.Database.GetProjectByID(r.Context(), workspace.ProjectID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("find project for conflicting workspace name %q: %s", createWorkspace.Name, err),
priorJob, err := api.Database.GetProvisionerJobByID(r.Context(), priorHistory.JobID)
if err == nil && !priorJob.CompletedAt.Valid {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: "a workspace build is already active",
})
return
}
// The project is fetched for clarity to the user on where the conflicting name may be.
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: fmt.Sprintf("workspace %q already exists in the %q project", createWorkspace.Name, project.Name),
Errors: []httpapi.Error{{
Field: "name",
Code: "exists",
}},
})
return
}
if !errors.Is(err, sql.ErrNoRows) {
priorHistoryID = uuid.NullUUID{
UUID: priorHistory.ID,
Valid: true,
}
} else if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace by name: %s", err.Error()),
Message: fmt.Sprintf("get prior workspace build: %s", err),
})
return
}
// Workspaces are created without any versions.
workspace, err = api.Database.InsertWorkspace(r.Context(), database.InsertWorkspaceParams{
ID: uuid.New(),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
OwnerID: apiKey.UserID,
ProjectID: project.ID,
Name: createWorkspace.Name,
var workspaceBuild database.WorkspaceBuild
var provisionerJob database.ProvisionerJob
// This must happen in a transaction to ensure history can be inserted, and
// the prior history can update it's "after" column to point at the new.
err = api.Database.InTx(func(db database.Store) error {
workspaceBuildID := uuid.New()
input, err := json.Marshal(workspaceProvisionJob{
WorkspaceBuildID: workspaceBuildID,
})
if err != nil {
return xerrors.Errorf("marshal provision job: %w", err)
}
provisionerJob, err = db.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{
ID: uuid.New(),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
InitiatorID: apiKey.UserID,
OrganizationID: project.OrganizationID,
Provisioner: project.Provisioner,
Type: database.ProvisionerJobTypeWorkspaceBuild,
StorageMethod: projectVersionJob.StorageMethod,
StorageSource: projectVersionJob.StorageSource,
Input: input,
})
if err != nil {
return xerrors.Errorf("insert provisioner job: %w", err)
}
workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{
ID: workspaceBuildID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
WorkspaceID: workspace.ID,
ProjectVersionID: projectVersion.ID,
BeforeID: priorHistoryID,
Name: namesgenerator.GetRandomName(1),
Initiator: apiKey.UserID,
Transition: createBuild.Transition,
JobID: provisionerJob.ID,
})
if err != nil {
return xerrors.Errorf("insert workspace build: %w", err)
}
if priorHistoryID.Valid {
// Update the prior history entries "after" column.
err = db.UpdateWorkspaceBuildByID(r.Context(), database.UpdateWorkspaceBuildByIDParams{
ID: priorHistory.ID,
ProvisionerState: priorHistory.ProvisionerState,
UpdatedAt: database.Now(),
AfterID: uuid.NullUUID{
UUID: workspaceBuild.ID,
Valid: true,
},
})
if err != nil {
return xerrors.Errorf("update prior workspace build: %w", err)
}
}
return nil
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert workspace: %s", err),
Message: err.Error(),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertWorkspace(workspace))
render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(provisionerJob)))
}
// Returns a single workspace.
func (*api) workspaceByUser(rw http.ResponseWriter, r *http.Request) {
func (api *api) workspaceBuildLatest(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertWorkspace(workspace))
}
// Returns all workspaces for a specific project.
func (api *api) workspacesByProject(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
project := httpmw.ProjectParam(r)
workspaces, err := api.Database.GetWorkspacesByProjectAndUserID(r.Context(), database.GetWorkspacesByProjectAndUserIDParams{
OwnerID: apiKey.UserID,
ProjectID: project.ID,
})
workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: "no workspace build found",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspaces: %s", err),
Message: fmt.Sprintf("get workspace build by name: %s", err),
})
return
}
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
apiWorkspaces := make([]Workspace, 0, len(workspaces))
for _, workspace := range workspaces {
apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiWorkspaces)
render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job)))
}
func (api *api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)
workspaceBuildName := chi.URLParam(r, "workspacebuildname")
workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDAndName(r.Context(), database.GetWorkspaceBuildByWorkspaceIDAndNameParams{
WorkspaceID: workspace.ID,
Name: workspaceBuildName,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("no workspace build found by name %q", workspaceBuildName),
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace build by name: %s", err),
})
return
}
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job)))
}
// Converts the internal workspace representation to a public external-facing model.
func convertWorkspace(workspace database.Workspace) Workspace {
return Workspace(workspace)
}

View File

@ -11,42 +11,34 @@ import (
"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"
)
func TestWorkspaces(t *testing.T) {
func TestWorkspace(t *testing.T) {
t.Parallel()
t.Run("ListNone", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
workspaces, err := client.Workspaces(context.Background(), "")
require.NoError(t, err)
require.NotNil(t, workspaces)
require.Len(t, workspaces, 0)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_ = coderdtest.CreateWorkspace(t, client, "", project.ID)
workspaces, err := client.Workspaces(context.Background(), "")
require.NoError(t, err)
require.Len(t, workspaces, 1)
})
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.Workspace(context.Background(), workspace.ID)
require.NoError(t, err)
}
func TestPostWorkspaceByUser(t *testing.T) {
func TestPostWorkspaceBuild(t *testing.T) {
t.Parallel()
t.Run("InvalidProject", func(t *testing.T) {
t.Run("NoProjectVersion", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
ProjectID: uuid.New(),
Name: "workspace",
user := coderdtest.CreateFirstUser(t, client)
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{
ProjectVersionID: uuid.New(),
Transition: database.WorkspaceTransitionStart,
})
require.Error(t, err)
var apiErr *codersdk.Error
@ -54,48 +46,46 @@ func TestPostWorkspaceByUser(t *testing.T) {
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
})
t.Run("NoProjectAccess", func(t *testing.T) {
t.Run("ProjectVersionFailedImport", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
anotherUser := coderd.CreateUserRequest{
Email: "another@user.org",
Username: "someuser",
Password: "somepass",
}
_, err := client.CreateUser(context.Background(), anotherUser)
require.NoError(t, err)
token, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: anotherUser.Email,
Password: anotherUser.Password,
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
Provision: []*proto.Provision_Response{{}},
})
require.NoError(t, err)
client.SessionToken = token.SessionToken
require.NoError(t, err)
_, err = client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
ProjectID: project.ID,
Name: "workspace",
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
_, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.Error(t, err)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
})
t.Run("AlreadyExists", func(t *testing.T) {
t.Run("AlreadyActive", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
ProjectID: project.ID,
Name: workspace.Name,
user := coderdtest.CreateFirstUser(t, client)
closeDaemon := coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
// Close here so workspace build doesn't process!
closeDaemon.Close()
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
_, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
_, err = client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.Error(t, err)
var apiErr *codersdk.Error
@ -103,50 +93,102 @@ func TestPostWorkspaceByUser(t *testing.T) {
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
t.Run("Create", func(t *testing.T) {
t.Run("UpdatePriorAfterField", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_ = coderdtest.CreateWorkspace(t, client, "", project.ID)
})
}
func TestWorkspaceByUser(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.Workspace(context.Background(), "", workspace.Name)
require.NoError(t, err)
}
func TestWorkspacesByProject(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
workspaces, err := client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
firstBuild, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
require.NotNil(t, workspaces)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_ = coderdtest.CreateWorkspace(t, client, "", project.ID)
workspaces, err := client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
coderdtest.AwaitWorkspaceBuildJob(t, client, firstBuild.ID)
secondBuild, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
require.Equal(t, firstBuild.ID.String(), secondBuild.BeforeID.String())
firstBuild, err = client.WorkspaceBuild(context.Background(), firstBuild.ID)
require.NoError(t, err)
require.Equal(t, secondBuild.ID.String(), firstBuild.AfterID.String())
})
}
func TestWorkspaceBuildLatest(t *testing.T) {
t.Parallel()
t.Run("None", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
_, err := client.WorkspaceBuildLatest(context.Background(), workspace.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("Found", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
_, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
_, err = client.WorkspaceBuildLatest(context.Background(), workspace.ID)
require.NoError(t, err)
})
}
func TestWorkspaceBuildByName(t *testing.T) {
t.Parallel()
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
_, err := client.WorkspaceBuildByName(context.Background(), workspace.ID, "something")
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("Found", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
_, err = client.WorkspaceBuildByName(context.Background(), workspace.ID, build.Name)
require.NoError(t, err)
require.NotNil(t, workspaces)
require.Len(t, workspaces, 1)
})
}

View File

@ -81,6 +81,20 @@ func (c *Client) request(ctx context.Context, method, path string, body interfac
// readBodyAsError reads the response as an httpapi.Message, and
// wraps it in a codersdk.Error type for easy marshaling.
func readBodyAsError(res *http.Response) error {
contentType := res.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "text/plain") {
resp, err := io.ReadAll(res.Body)
if err != nil {
return xerrors.Errorf("read body: %w", err)
}
return &Error{
statusCode: res.StatusCode,
Response: httpapi.Response{
Message: string(resp),
},
}
}
var m httpapi.Response
err := json.NewDecoder(res.Body).Decode(&m)
if err != nil {

View File

@ -4,8 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"github.com/coder/coder/coderd"
)
@ -14,22 +14,36 @@ const (
ContentTypeTar = "application/x-tar"
)
func (c *Client) UploadFile(ctx context.Context, contentType string, content []byte) (coderd.UploadFileResponse, error) {
res, err := c.request(ctx, http.MethodPost, "/api/v2/upload", content, func(r *http.Request) {
// 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) {
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.UploadFileResponse{}, err
return coderd.UploadResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated && res.StatusCode != http.StatusOK {
return coderd.UploadFileResponse{}, readBodyAsError(res)
return coderd.UploadResponse{}, readBodyAsError(res)
}
var resp coderd.UploadFileResponse
var resp coderd.UploadResponse
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// DownloadURL returns
func (c *Client) DownloadURL(asset string) (*url.URL, error) {
return c.URL.Parse(fmt.Sprintf("/api/v2/downloads/%s", asset))
// Download fetches a file by uploaded hash.
func (c *Client) Download(ctx context.Context, hash string) ([]byte, string, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/files/%s", hash), 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
}

View File

@ -1,28 +0,0 @@
package codersdk_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
)
func TestUpload(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.UploadFile(context.Background(), "wow", []byte{})
require.Error(t, err)
})
t.Run("Upload", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.UploadFile(context.Background(), codersdk.ContentTypeTar, []byte{'a'})
require.NoError(t, err)
})
}

94
codersdk/organizations.go Normal file
View File

@ -0,0 +1,94 @@
package codersdk
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/coder/coder/coderd"
)
func (c *Client) Organization(ctx context.Context, id string) (coderd.Organization, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s", id), nil)
if err != nil {
return coderd.Organization{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return coderd.Organization{}, readBodyAsError(res)
}
var organization coderd.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) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons", organization), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var daemons []coderd.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) {
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/projectversions", organization), req)
if err != nil {
return coderd.ProjectVersion{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return coderd.ProjectVersion{}, readBodyAsError(res)
}
var projectVersion coderd.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) {
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/projects", organization), request)
if err != nil {
return coderd.Project{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return coderd.Project{}, readBodyAsError(res)
}
var project coderd.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) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/projects", organization), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var projects []coderd.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) {
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
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return coderd.Project{}, readBodyAsError(res)
}
var project coderd.Project
return project, json.NewDecoder(res.Body).Decode(&project)
}

48
codersdk/parameters.go Normal file
View File

@ -0,0 +1,48 @@
package codersdk
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/coder/coder/coderd"
)
func (c *Client) CreateParameter(ctx context.Context, scope coderd.ParameterScope, id string, req coderd.CreateParameterRequest) (coderd.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
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return coderd.Parameter{}, readBodyAsError(res)
}
var param coderd.Parameter
return param, json.NewDecoder(res.Body).Decode(&param)
}
func (c *Client) DeleteParameter(ctx context.Context, scope coderd.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
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return readBodyAsError(res)
}
return nil
}
func (c *Client) Parameters(ctx context.Context, scope coderd.ParameterScope, id string) ([]coderd.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
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var parameters []coderd.Parameter
return parameters, json.NewDecoder(res.Body).Decode(&parameters)
}

View File

@ -1,95 +0,0 @@
package codersdk
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"github.com/coder/coder/coderd"
)
// CreateProjectImportJob creates a new import job in the organization provided.
// ProjectImportJob is not associated with a project by default. Projects
// are created from import.
func (c *Client) CreateProjectImportJob(ctx context.Context, organization string, req coderd.CreateProjectImportJobRequest) (coderd.ProvisionerJob, error) {
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/projectimport/%s", organization), req)
if err != nil {
return coderd.ProvisionerJob{}, err
}
if res.StatusCode != http.StatusCreated {
defer res.Body.Close()
return coderd.ProvisionerJob{}, readBodyAsError(res)
}
var job coderd.ProvisionerJob
return job, json.NewDecoder(res.Body).Decode(&job)
}
// ProjectImportJob returns an import job by ID.
func (c *Client) ProjectImportJob(ctx context.Context, organization string, job uuid.UUID) (coderd.ProvisionerJob, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectimport/%s/%s", organization, job), nil)
if err != nil {
return coderd.ProvisionerJob{}, nil
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return coderd.ProvisionerJob{}, readBodyAsError(res)
}
var resp coderd.ProvisionerJob
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// ProjectImportJobLogsBefore returns logs that occurred before a specific time.
func (c *Client) ProjectImportJobLogsBefore(ctx context.Context, organization string, job uuid.UUID, before time.Time) ([]coderd.ProvisionerJobLog, error) {
return c.provisionerJobLogsBefore(ctx, "projectimport", organization, job, before)
}
// ProjectImportJobLogsAfter streams logs for a project import operation that occurred after a specific time.
func (c *Client) ProjectImportJobLogsAfter(ctx context.Context, organization string, job uuid.UUID, after time.Time) (<-chan coderd.ProvisionerJobLog, error) {
return c.provisionerJobLogsAfter(ctx, "projectimport", organization, job, after)
}
// ProjectImportJobSchemas returns schemas for an import job by ID.
func (c *Client) ProjectImportJobSchemas(ctx context.Context, organization string, job uuid.UUID) ([]coderd.ParameterSchema, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectimport/%s/%s/schemas", organization, job), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var params []coderd.ParameterSchema
return params, json.NewDecoder(res.Body).Decode(&params)
}
// ProjectImportJobParameters returns computed parameters for a project import job.
func (c *Client) ProjectImportJobParameters(ctx context.Context, organization string, job uuid.UUID) ([]coderd.ComputedParameterValue, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectimport/%s/%s/parameters", organization, job), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var params []coderd.ComputedParameterValue
return params, json.NewDecoder(res.Body).Decode(&params)
}
// ProjectImportJobResources returns resources for a project import job.
func (c *Client) ProjectImportJobResources(ctx context.Context, organization string, job uuid.UUID) ([]coderd.ProvisionerJobResource, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectimport/%s/%s/resources", organization, job), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var resources []coderd.ProvisionerJobResource
return resources, json.NewDecoder(res.Body).Decode(&resources)
}

View File

@ -1,147 +0,0 @@
package codersdk_test
import (
"context"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
)
func TestCreateProjectImportJob(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.CreateProjectImportJob(context.Background(), "", coderd.CreateProjectImportJobRequest{})
require.Error(t, err)
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
})
}
func TestProjectImportJob(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.ProjectImportJob(context.Background(), "", uuid.New())
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
_, err := client.ProjectImportJob(context.Background(), user.Organization, job.ID)
require.NoError(t, err)
})
}
func TestProjectImportJobLogsBefore(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.ProjectImportJobLogsBefore(context.Background(), "", uuid.New(), time.Time{})
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
before := time.Now()
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{
Parse: []*proto.Parse_Response{{
Type: &proto.Parse_Response_Log{
Log: &proto.Log{
Output: "hello",
},
},
}},
Provision: echo.ProvisionComplete,
})
logs, err := client.ProjectImportJobLogsAfter(context.Background(), user.Organization, job.ID, before)
require.NoError(t, err)
<-logs
})
}
func TestProjectImportJobLogsAfter(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.ProjectImportJobLogsAfter(context.Background(), "", uuid.New(), time.Time{})
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{
Parse: []*proto.Parse_Response{{
Type: &proto.Parse_Response_Log{
Log: &proto.Log{
Output: "hello",
},
},
}, {
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{},
},
}},
Provision: echo.ProvisionComplete,
})
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
logs, err := client.ProjectImportJobLogsBefore(context.Background(), user.Organization, job.ID, time.Time{})
require.NoError(t, err)
require.Len(t, logs, 1)
})
}
func TestProjectImportJobSchemas(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.ProjectImportJobSchemas(context.Background(), "", uuid.New())
require.Error(t, err)
})
}
func TestProjectImportJobParameters(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.ProjectImportJobParameters(context.Background(), "", uuid.New())
require.Error(t, err)
})
}
func TestProjectImportJobResources(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.ProjectImportJobResources(context.Background(), "", uuid.New())
require.Error(t, err)
})
}

View File

@ -6,32 +6,14 @@ import (
"fmt"
"net/http"
"github.com/google/uuid"
"github.com/coder/coder/coderd"
)
// Projects lists projects inside an organization.
// If organization is an empty string, all projects will be returned
// for the authenticated user.
func (c *Client) Projects(ctx context.Context, organization string) ([]coderd.Project, error) {
route := "/api/v2/projects"
if organization != "" {
route = fmt.Sprintf("/api/v2/projects/%s", organization)
}
res, err := c.request(ctx, http.MethodGet, route, nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var projects []coderd.Project
return projects, json.NewDecoder(res.Body).Decode(&projects)
}
// Project returns a single project.
func (c *Client) Project(ctx context.Context, organization, project string) (coderd.Project, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/%s", organization, project), nil)
func (c *Client) Project(ctx context.Context, project uuid.UUID) (coderd.Project, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s", project), nil)
if err != nil {
return coderd.Project{}, nil
}
@ -43,23 +25,9 @@ func (c *Client) Project(ctx context.Context, organization, project string) (cod
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// CreateProject creates a new project inside an organization.
func (c *Client) CreateProject(ctx context.Context, organization string, request coderd.CreateProjectRequest) (coderd.Project, error) {
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/projects/%s", organization), request)
if err != nil {
return coderd.Project{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return coderd.Project{}, readBodyAsError(res)
}
var project coderd.Project
return project, json.NewDecoder(res.Body).Decode(&project)
}
// ProjectVersions lists versions of a project.
func (c *Client) ProjectVersions(ctx context.Context, organization, project string) ([]coderd.ProjectVersion, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/%s/versions", organization, project), nil)
// ProjectVersionsByProject lists versions associated with a project.
func (c *Client) ProjectVersionsByProject(ctx context.Context, project uuid.UUID) ([]coderd.ProjectVersion, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/versions", project), nil)
if err != nil {
return nil, err
}
@ -71,9 +39,10 @@ func (c *Client) ProjectVersions(ctx context.Context, organization, project stri
return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion)
}
// ProjectVersion returns project version by name.
func (c *Client) ProjectVersion(ctx context.Context, organization, project, version string) (coderd.ProjectVersion, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/%s/versions/%s", organization, project, version), nil)
// 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) {
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
}
@ -84,45 +53,3 @@ func (c *Client) ProjectVersion(ctx context.Context, organization, project, vers
var projectVersion coderd.ProjectVersion
return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion)
}
// CreateProjectVersion inserts a new version for the project.
func (c *Client) CreateProjectVersion(ctx context.Context, organization, project string, request coderd.CreateProjectVersionRequest) (coderd.ProjectVersion, error) {
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/projects/%s/%s/versions", organization, project), request)
if err != nil {
return coderd.ProjectVersion{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return coderd.ProjectVersion{}, readBodyAsError(res)
}
var projectVersion coderd.ProjectVersion
return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion)
}
// ProjectParameters returns parameters scoped to a project.
func (c *Client) ProjectParameters(ctx context.Context, organization, project string) ([]coderd.ParameterValue, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/%s/parameters", organization, project), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var params []coderd.ParameterValue
return params, json.NewDecoder(res.Body).Decode(&params)
}
// CreateProjectParameter creates a new parameter value scoped to a project.
func (c *Client) CreateProjectParameter(ctx context.Context, organization, project string, req coderd.CreateParameterValueRequest) (coderd.ParameterValue, error) {
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/projects/%s/%s/parameters", organization, project), req)
if err != nil {
return coderd.ParameterValue{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return coderd.ParameterValue{}, readBodyAsError(res)
}
var param coderd.ParameterValue
return param, json.NewDecoder(res.Body).Decode(&param)
}

View File

@ -1,179 +0,0 @@
package codersdk_test
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/database"
)
func TestProjects(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.Projects(context.Background(), "")
require.Error(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.Projects(context.Background(), "")
require.NoError(t, err)
})
}
func TestProject(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.Project(context.Background(), "", "")
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_, err := client.Project(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
})
}
func TestCreateProject(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.CreateProject(context.Background(), "org", coderd.CreateProjectRequest{
Name: "something",
VersionImportJobID: uuid.New(),
})
require.Error(t, err)
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
_ = coderdtest.CreateProject(t, client, user.Organization, job.ID)
})
}
func TestProjectVersions(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.ProjectVersions(context.Background(), "some", "project")
require.Error(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_, err := client.ProjectVersions(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
})
}
func TestProjectVersion(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.ProjectVersion(context.Background(), "some", "project", "version")
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_, err := client.ProjectVersion(context.Background(), user.Organization, project.Name, project.ActiveVersionID.String())
require.NoError(t, err)
})
}
func TestCreateProjectVersion(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.CreateProjectVersion(context.Background(), "some", "project", coderd.CreateProjectVersionRequest{})
require.Error(t, err)
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_, err := client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
ImportJobID: job.ID,
})
require.NoError(t, err)
})
}
func TestProjectParameters(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.ProjectParameters(context.Background(), "some", "project")
require.Error(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_, err := client.ProjectParameters(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
})
}
func TestCreateProjectParameter(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.CreateProjectParameter(context.Background(), "some", "project", coderd.CreateParameterValueRequest{})
require.Error(t, err)
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_, err := client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{
Name: "example",
SourceValue: "source-value",
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable,
})
require.NoError(t, err)
})
}

View File

@ -0,0 +1,79 @@
package codersdk
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"github.com/coder/coder/coderd"
)
// ProjectVersion returns a project version by ID.
func (c *Client) ProjectVersion(ctx context.Context, id uuid.UUID) (coderd.ProjectVersion, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s", id), nil)
if err != nil {
return coderd.ProjectVersion{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return coderd.ProjectVersion{}, readBodyAsError(res)
}
var version coderd.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) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s/schema", version), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var params []coderd.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) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s/parameters", version), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var params []coderd.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) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s/resources", version), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var resources []coderd.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) {
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) {
return c.provisionerJobLogsAfter(ctx, fmt.Sprintf("/api/v2/projectversions/%s/logs", version), after)
}

View File

@ -10,7 +10,6 @@ import (
"strconv"
"time"
"github.com/google/uuid"
"github.com/hashicorp/yamux"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
@ -20,23 +19,9 @@ import (
"github.com/coder/coder/provisionersdk"
)
// ProvisionerDaemons returns registered provisionerd instances.
func (c *Client) ProvisionerDaemons(ctx context.Context) ([]coderd.ProvisionerDaemon, error) {
res, err := c.request(ctx, http.MethodGet, "/api/v2/provisioners/daemons", nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var daemons []coderd.ProvisionerDaemon
return daemons, json.NewDecoder(res.Body).Decode(&daemons)
}
// ProvisionerDaemonClient returns the gRPC service for a provisioner daemon implementation.
func (c *Client) ProvisionerDaemonClient(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
serverURL, err := c.URL.Parse("/api/v2/provisioners/daemons/serve")
// 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")
if err != nil {
return nil, xerrors.Errorf("parse url: %w", err)
}
@ -63,12 +48,12 @@ func (c *Client) ProvisionerDaemonClient(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, jobType, organization string, job uuid.UUID, before time.Time) ([]coderd.ProvisionerJobLog, error) {
func (c *Client) provisionerJobLogsBefore(ctx context.Context, path string, before time.Time) ([]coderd.ProvisionerJobLog, error) {
values := url.Values{}
if !before.IsZero() {
values["before"] = []string{strconv.FormatInt(before.UTC().UnixMilli(), 10)}
}
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/%s/%s/%s/logs?%s", jobType, organization, job, values.Encode()), nil)
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("%s?%s", path, values.Encode()), nil)
if err != nil {
return nil, err
}
@ -82,12 +67,12 @@ func (c *Client) provisionerJobLogsBefore(ctx context.Context, jobType, organiza
}
// provisionerJobLogsAfter streams logs that occurred after a specific time.
func (c *Client) provisionerJobLogsAfter(ctx context.Context, jobType, organization string, job uuid.UUID, after time.Time) (<-chan coderd.ProvisionerJobLog, error) {
func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after time.Time) (<-chan coderd.ProvisionerJobLog, error) {
afterQuery := ""
if !after.IsZero() {
afterQuery = fmt.Sprintf("&after=%d", after.UTC().UnixMilli())
}
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/%s/%s/%s/logs?follow%s", jobType, organization, job, afterQuery), nil)
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("%s?follow%s", path, afterQuery), nil)
if err != nil {
return nil, err
}

View File

@ -1,46 +0,0 @@
package codersdk_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/provisionerd/proto"
)
func TestProvisionerDaemons(t *testing.T) {
t.Parallel()
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.ProvisionerDaemons(context.Background())
require.NoError(t, err)
})
}
func TestProvisionerDaemonClient(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
ctx, cancelFunc := context.WithCancel(context.Background())
daemon, err := client.ProvisionerDaemonClient(ctx)
require.NoError(t, err)
cancelFunc()
_, err = daemon.AcquireJob(context.Background(), &proto.Empty{})
require.Error(t, err)
})
t.Run("Connect", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
daemon, err := client.ProvisionerDaemonClient(ctx)
require.NoError(t, err)
_, err = daemon.AcquireJob(ctx, &proto.Empty{})
require.NoError(t, err)
})
}

View File

@ -9,10 +9,9 @@ import (
"github.com/coder/coder/coderd"
)
// HasInitialUser returns whether the initial user has already been
// created or not.
func (c *Client) HasInitialUser(ctx context.Context) (bool, error) {
res, err := c.request(ctx, http.MethodGet, "/api/v2/user", nil)
// 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)
if err != nil {
return false, err
}
@ -26,20 +25,19 @@ func (c *Client) HasInitialUser(ctx context.Context) (bool, error) {
return true, nil
}
// CreateInitialUser 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) CreateInitialUser(ctx context.Context, req coderd.CreateInitialUserRequest) (coderd.User, error) {
res, err := c.request(ctx, http.MethodPost, "/api/v2/user", req)
// 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) {
res, err := c.request(ctx, http.MethodPost, "/api/v2/users/first", req)
if err != nil {
return coderd.User{}, err
return coderd.CreateFirstUserResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return coderd.User{}, readBodyAsError(res)
return coderd.CreateFirstUserResponse{}, readBodyAsError(res)
}
var user coderd.User
return user, json.NewDecoder(res.Body).Decode(&user)
var resp coderd.CreateFirstUserResponse
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// CreateUser creates a new user.
@ -56,9 +54,12 @@ func (c *Client) CreateUser(ctx context.Context, req coderd.CreateUserRequest) (
return user, json.NewDecoder(res.Body).Decode(&user)
}
// CreateAPIKey calls the /api-key API
func (c *Client) CreateAPIKey(ctx context.Context) (*coderd.GenerateAPIKeyResponse, error) {
res, err := c.request(ctx, http.MethodPost, "/api/v2/users/me/keys", nil)
// CreateAPIKey generates an API key for the user ID provided.
func (c *Client) CreateAPIKey(ctx context.Context, id string) (*coderd.GenerateAPIKeyResponse, error) {
if id == "" {
id = "me"
}
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", id), nil)
if err != nil {
return nil, err
}
@ -73,7 +74,7 @@ func (c *Client) CreateAPIKey(ctx context.Context) (*coderd.GenerateAPIKeyRespon
// 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) {
res, err := c.request(ctx, http.MethodPost, "/api/v2/login", req)
res, err := c.request(ctx, http.MethodPost, "/api/v2/users/login", req)
if err != nil {
return coderd.LoginWithPasswordResponse{}, err
}
@ -94,7 +95,7 @@ func (c *Client) LoginWithPassword(ctx context.Context, req coderd.LoginWithPass
func (c *Client) Logout(ctx context.Context) error {
// Since `LoginWithPassword` doesn't actually set a SessionToken
// (it requires a call to SetSessionToken), this is essentially a no-op
res, err := c.request(ctx, http.MethodPost, "/api/v2/logout", nil)
res, err := c.request(ctx, http.MethodPost, "/api/v2/users/logout", nil)
if err != nil {
return err
}
@ -120,8 +121,8 @@ func (c *Client) User(ctx context.Context, id string) (coderd.User, error) {
return user, json.NewDecoder(res.Body).Decode(&user)
}
// UserOrganizations fetches organizations a user is part of.
func (c *Client) UserOrganizations(ctx context.Context, id string) ([]coderd.Organization, error) {
// OrganizationsByUser returns all organizations the user is a member of.
func (c *Client) OrganizationsByUser(ctx context.Context, id string) ([]coderd.Organization, error) {
if id == "" {
id = "me"
}
@ -130,9 +131,92 @@ func (c *Client) UserOrganizations(ctx context.Context, id string) ([]coderd.Org
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
if res.StatusCode > http.StatusOK {
return nil, readBodyAsError(res)
}
var orgs []coderd.Organization
return orgs, json.NewDecoder(res.Body).Decode(&orgs)
}
func (c *Client) OrganizationByName(ctx context.Context, user, name string) (coderd.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
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return coderd.Organization{}, readBodyAsError(res)
}
var org coderd.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) {
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
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return coderd.Organization{}, readBodyAsError(res)
}
var org coderd.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) {
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
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return coderd.Workspace{}, readBodyAsError(res)
}
var workspace coderd.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) {
if user == "" {
user = "me"
}
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspaces", user), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var workspaces []coderd.Workspace
return workspaces, json.NewDecoder(res.Body).Decode(&workspaces)
}
func (c *Client) WorkspaceByName(ctx context.Context, user, name string) (coderd.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
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return coderd.Workspace{}, readBodyAsError(res)
}
var workspace coderd.Workspace
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
}

View File

@ -1,133 +0,0 @@
package codersdk_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
)
func TestHasInitialUser(t *testing.T) {
t.Parallel()
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
has, err := client.HasInitialUser(context.Background())
require.NoError(t, err)
require.False(t, has)
})
t.Run("Found", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
has, err := client.HasInitialUser(context.Background())
require.NoError(t, err)
require.True(t, has)
})
}
func TestCreateInitialUser(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{})
require.Error(t, err)
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
})
}
func TestCreateUser(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{})
require.Error(t, err)
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: "example@coder.com",
Username: "something",
Password: "password",
})
require.NoError(t, err)
})
}
func TestLoginWithPassword(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{})
require.Error(t, err)
})
t.Run("Success", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
_, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: user.Email,
Password: user.Password,
})
require.NoError(t, err)
})
}
func TestLogout(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
err := client.Logout(context.Background())
require.NoError(t, err)
}
func TestUser(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.User(context.Background(), "")
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.User(context.Background(), "")
require.NoError(t, err)
})
}
func TestUserOrganizations(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.UserOrganizations(context.Background(), "")
require.Error(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.UserOrganizations(context.Background(), "")
require.NoError(t, err)
})
}

View File

@ -1,37 +0,0 @@
package codersdk_test
import (
"bytes"
"context"
"io"
"net/http"
"testing"
"cloud.google.com/go/compute/metadata"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
)
func TestAuthenticateWorkspaceAgentUsingGoogleCloudIdentity(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", metadata.NewClient(&http.Client{
Transport: roundTripper(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader([]byte("sometoken"))),
}, nil
}),
}))
require.Error(t, err)
})
}
type roundTripper func(req *http.Request) (*http.Response, error)
func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return r(req)
}

View File

@ -0,0 +1,52 @@
package codersdk
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"github.com/coder/coder/coderd"
)
// 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) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspacebuilds/%s", id), nil)
if err != nil {
return coderd.WorkspaceBuild{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return coderd.WorkspaceBuild{}, readBodyAsError(res)
}
var workspaceBuild coderd.WorkspaceBuild
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) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspacebuilds/%s/resources", build), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var resources []coderd.WorkspaceResource
return resources, json.NewDecoder(res.Body).Decode(&resources)
}
// WorkspaceBuildLogsBefore returns logs that occurred before a specific time.
func (c *Client) WorkspaceBuildLogsBefore(ctx context.Context, version uuid.UUID, before time.Time) ([]coderd.ProvisionerJobLog, error) {
return c.provisionerJobLogsBefore(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", version), before)
}
// WorkspaceBuildLogsAfter streams logs for a workspace build that occurred after a specific time.
func (c *Client) WorkspaceBuildLogsAfter(ctx context.Context, version uuid.UUID, after time.Time) (<-chan coderd.ProvisionerJobLog, error) {
return c.provisionerJobLogsAfter(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", version), after)
}

View File

@ -12,11 +12,11 @@ import (
"github.com/coder/coder/coderd"
)
// AuthenticateWorkspaceAgentUsingGoogleCloudIdentity uses the Google Compute Engine Metadata API to
// 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) AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(ctx context.Context, serviceAccount string, gcpClient *metadata.Client) (coderd.WorkspaceAgentAuthenticateResponse, error) {
func (c *Client) AuthWorkspaceGoogleInstanceIdentity(ctx context.Context, serviceAccount string, gcpClient *metadata.Client) (coderd.WorkspaceAgentAuthenticateResponse, error) {
if serviceAccount == "" {
// This is the default name specified by Google.
serviceAccount = "default"
@ -29,7 +29,7 @@ func (c *Client) AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(ctx context.
if err != nil {
return coderd.WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("get metadata identity: %w", err)
}
res, err := c.request(ctx, http.MethodPost, "/api/v2/workspaceagent/authenticate/google-instance-identity", coderd.GoogleInstanceIdentityToken{
res, err := c.request(ctx, http.MethodPost, "/api/v2/workspaceresources/auth/google-instance-identity", coderd.GoogleInstanceIdentityToken{
JSONWebToken: jwt,
})
if err != nil {

View File

@ -0,0 +1,110 @@
package codersdk
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"github.com/google/uuid"
"github.com/hashicorp/yamux"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"github.com/coder/coder/coderd"
"github.com/coder/coder/httpmw"
"github.com/coder/coder/peer"
"github.com/coder/coder/peerbroker"
"github.com/coder/coder/peerbroker/proto"
"github.com/coder/coder/provisionersdk"
)
func (c *Client) WorkspaceResource(ctx context.Context, id uuid.UUID) (coderd.WorkspaceResource, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceresources/%s", id), nil)
if err != nil {
return coderd.WorkspaceResource{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return coderd.WorkspaceResource{}, readBodyAsError(res)
}
var resource coderd.WorkspaceResource
return resource, json.NewDecoder(res.Body).Decode(&resource)
}
// DialWorkspaceAgent creates a connection to the specified resource.
func (c *Client) DialWorkspaceAgent(ctx context.Context, resource uuid.UUID) (proto.DRPCPeerBrokerClient, error) {
serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/workspaceresources/%s/dial", resource.String()))
if err != nil {
return nil, xerrors.Errorf("parse url: %w", err)
}
jar, err := cookiejar.New(nil)
if err != nil {
return nil, xerrors.Errorf("create cookie jar: %w", err)
}
jar.SetCookies(serverURL, []*http.Cookie{{
Name: httpmw.AuthCookie,
Value: c.SessionToken,
}})
httpClient := &http.Client{
Jar: jar,
}
conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{
HTTPClient: httpClient,
// Need to disable compression to avoid a data-race.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
if res == nil {
return nil, err
}
return nil, readBodyAsError(res)
}
config := yamux.DefaultConfig()
config.LogOutput = io.Discard
session, err := yamux.Client(websocket.NetConn(ctx, conn, websocket.MessageBinary), config)
if err != nil {
return nil, xerrors.Errorf("multiplex client: %w", err)
}
return proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(session)), nil
}
// ListenWorkspaceAgent connects as a workspace agent.
// It obtains the agent ID based off the session token.
func (c *Client) ListenWorkspaceAgent(ctx context.Context, opts *peer.ConnOptions) (*peerbroker.Listener, error) {
serverURL, err := c.URL.Parse("/api/v2/workspaceresources/agent")
if err != nil {
return nil, xerrors.Errorf("parse url: %w", err)
}
jar, err := cookiejar.New(nil)
if err != nil {
return nil, xerrors.Errorf("create cookie jar: %w", err)
}
jar.SetCookies(serverURL, []*http.Cookie{{
Name: httpmw.AuthCookie,
Value: c.SessionToken,
}})
httpClient := &http.Client{
Jar: jar,
}
conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{
HTTPClient: httpClient,
// Need to disable compression to avoid a data-race.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
if res == nil {
return nil, err
}
return nil, readBodyAsError(res)
}
config := yamux.DefaultConfig()
config.LogOutput = io.Discard
session, err := yamux.Client(websocket.NetConn(ctx, conn, websocket.MessageBinary), config)
if err != nil {
return nil, xerrors.Errorf("multiplex client: %w", err)
}
return peerbroker.Listen(session, nil, opts)
}

View File

@ -5,53 +5,15 @@ import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"github.com/coder/coder/coderd"
)
// Workspaces returns all workspaces the authenticated session has access to.
// If owner is specified, all workspaces for an organization will be returned.
// If owner is empty, all workspaces the caller has access to will be returned.
func (c *Client) Workspaces(ctx context.Context, user string) ([]coderd.Workspace, error) {
route := "/api/v2/workspaces"
if user != "" {
route += fmt.Sprintf("/%s", user)
}
res, err := c.request(ctx, http.MethodGet, route, nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var workspaces []coderd.Workspace
return workspaces, json.NewDecoder(res.Body).Decode(&workspaces)
}
// WorkspacesByProject lists all workspaces for a specific project.
func (c *Client) WorkspacesByProject(ctx context.Context, organization, project string) ([]coderd.Workspace, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/%s/workspaces", organization, project), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var workspaces []coderd.Workspace
return workspaces, json.NewDecoder(res.Body).Decode(&workspaces)
}
// Workspace returns a single workspace by owner and name.
func (c *Client) Workspace(ctx context.Context, owner, name string) (coderd.Workspace, error) {
if owner == "" {
owner = "me"
}
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/%s", owner, name), nil)
// Workspace returns a single workspace.
func (c *Client) Workspace(ctx context.Context, id uuid.UUID) (coderd.Workspace, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s", id), nil)
if err != nil {
return coderd.Workspace{}, err
}
@ -63,12 +25,8 @@ func (c *Client) Workspace(ctx context.Context, owner, name string) (coderd.Work
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
}
// ListWorkspaceHistory returns historical data for workspace builds.
func (c *Client) ListWorkspaceHistory(ctx context.Context, owner, workspace string) ([]coderd.WorkspaceHistory, error) {
if owner == "" {
owner = "me"
}
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/%s/version", owner, workspace), nil)
func (c *Client) WorkspaceBuilds(ctx context.Context, workspace uuid.UUID) ([]coderd.WorkspaceBuild, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds", workspace), nil)
if err != nil {
return nil, err
}
@ -76,84 +34,46 @@ func (c *Client) ListWorkspaceHistory(ctx context.Context, owner, workspace stri
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var workspaceHistory []coderd.WorkspaceHistory
return workspaceHistory, json.NewDecoder(res.Body).Decode(&workspaceHistory)
var workspaceBuild []coderd.WorkspaceBuild
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
}
// WorkspaceHistory returns a single workspace history for a workspace.
// If history is "", the latest version is returned.
func (c *Client) WorkspaceHistory(ctx context.Context, owner, workspace, history string) (coderd.WorkspaceHistory, error) {
if owner == "" {
owner = "me"
}
if history == "" {
history = "latest"
}
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/%s/version/%s", owner, workspace, history), nil)
// 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) {
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaces/%s/builds", workspace), request)
if err != nil {
return coderd.WorkspaceHistory{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return coderd.WorkspaceHistory{}, readBodyAsError(res)
}
var workspaceHistory coderd.WorkspaceHistory
return workspaceHistory, json.NewDecoder(res.Body).Decode(&workspaceHistory)
}
// CreateWorkspace creates a new workspace for the project specified.
func (c *Client) CreateWorkspace(ctx context.Context, user string, request coderd.CreateWorkspaceRequest) (coderd.Workspace, error) {
if user == "" {
user = "me"
}
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaces/%s", user), request)
if err != nil {
return coderd.Workspace{}, err
return coderd.WorkspaceBuild{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return coderd.Workspace{}, readBodyAsError(res)
return coderd.WorkspaceBuild{}, readBodyAsError(res)
}
var workspace coderd.Workspace
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
var workspaceBuild coderd.WorkspaceBuild
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
}
// CreateWorkspaceHistory queues a new build to occur for a workspace.
func (c *Client) CreateWorkspaceHistory(ctx context.Context, owner, workspace string, request coderd.CreateWorkspaceHistoryRequest) (coderd.WorkspaceHistory, error) {
if owner == "" {
owner = "me"
}
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaces/%s/%s/version", owner, workspace), request)
func (c *Client) WorkspaceBuildByName(ctx context.Context, workspace uuid.UUID, name string) (coderd.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.WorkspaceHistory{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return coderd.WorkspaceHistory{}, readBodyAsError(res)
}
var workspaceHistory coderd.WorkspaceHistory
return workspaceHistory, json.NewDecoder(res.Body).Decode(&workspaceHistory)
}
func (c *Client) WorkspaceProvisionJob(ctx context.Context, organization string, job uuid.UUID) (coderd.ProvisionerJob, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceprovision/%s/%s", organization, job), nil)
if err != nil {
return coderd.ProvisionerJob{}, nil
return coderd.WorkspaceBuild{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return coderd.ProvisionerJob{}, readBodyAsError(res)
return coderd.WorkspaceBuild{}, readBodyAsError(res)
}
var resp coderd.ProvisionerJob
return resp, json.NewDecoder(res.Body).Decode(&resp)
var workspaceBuild coderd.WorkspaceBuild
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
}
// WorkspaceProvisionJobLogsBefore returns logs that occurred before a specific time.
func (c *Client) WorkspaceProvisionJobLogsBefore(ctx context.Context, organization string, job uuid.UUID, before time.Time) ([]coderd.ProvisionerJobLog, error) {
return c.provisionerJobLogsBefore(ctx, "workspaceprovision", organization, job, before)
}
// WorkspaceProvisionJobLogsAfter streams logs for a workspace provision operation that occurred after a specific time.
func (c *Client) WorkspaceProvisionJobLogsAfter(ctx context.Context, organization string, job uuid.UUID, after time.Time) (<-chan coderd.ProvisionerJobLog, error) {
return c.provisionerJobLogsAfter(ctx, "workspaceprovision", organization, job, after)
func (c *Client) WorkspaceBuildLatest(ctx context.Context, workspace uuid.UUID) (coderd.WorkspaceBuild, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds/latest", workspace), nil)
if err != nil {
return coderd.WorkspaceBuild{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return coderd.WorkspaceBuild{}, readBodyAsError(res)
}
var workspaceBuild coderd.WorkspaceBuild
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
}

View File

@ -1,163 +0,0 @@
package codersdk_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/database"
)
func TestWorkspaces(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.Workspaces(context.Background(), "")
require.Error(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.Workspaces(context.Background(), "")
require.NoError(t, err)
})
}
func TestWorkspacesByProject(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.WorkspacesByProject(context.Background(), "", "")
require.Error(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_, err := client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
})
}
func TestWorkspace(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.Workspace(context.Background(), "", "")
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.Workspace(context.Background(), "", workspace.Name)
require.NoError(t, err)
})
}
func TestListWorkspaceHistory(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.ListWorkspaceHistory(context.Background(), "", "")
require.Error(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.ListWorkspaceHistory(context.Background(), "", workspace.Name)
require.NoError(t, err)
})
}
func TestWorkspaceHistory(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.WorkspaceHistory(context.Background(), "", "", "")
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
})
}
func TestCreateWorkspace(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{})
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
_ = coderdtest.CreateWorkspace(t, client, "", project.ID)
})
}
func TestCreateWorkspaceHistory(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_, err := client.CreateWorkspaceHistory(context.Background(), "", "", coderd.CreateWorkspaceHistoryRequest{})
require.Error(t, err)
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.NewProvisionerDaemon(t, client)
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: project.ActiveVersionID,
Transition: database.WorkspaceTransitionStart,
})
require.NoError(t, err)
})
}

View File

@ -28,9 +28,9 @@ func New() database.Store {
provisionerJobs: make([]database.ProvisionerJob, 0),
provisionerJobLog: make([]database.ProvisionerJobLog, 0),
workspace: make([]database.Workspace, 0),
provisionerJobResource: make([]database.ProvisionerJobResource, 0),
workspaceHistory: make([]database.WorkspaceHistory, 0),
provisionerJobAgent: make([]database.ProvisionerJobAgent, 0),
provisionerJobResource: make([]database.WorkspaceResource, 0),
workspaceBuild: make([]database.WorkspaceBuild, 0),
provisionerJobAgent: make([]database.WorkspaceAgent, 0),
}
}
@ -52,11 +52,11 @@ type fakeQuerier struct {
projectVersion []database.ProjectVersion
provisionerDaemons []database.ProvisionerDaemon
provisionerJobs []database.ProvisionerJob
provisionerJobAgent []database.ProvisionerJobAgent
provisionerJobResource []database.ProvisionerJobResource
provisionerJobAgent []database.WorkspaceAgent
provisionerJobResource []database.WorkspaceResource
provisionerJobLog []database.ProvisionerJobLog
workspace []database.Workspace
workspaceHistory []database.WorkspaceHistory
workspaceBuild []database.WorkspaceBuild
}
// InTx doesn't rollback data properly for in-memory yet.
@ -92,6 +92,21 @@ func (q *fakeQuerier) AcquireProvisionerJob(_ context.Context, arg database.Acqu
return database.ProvisionerJob{}, sql.ErrNoRows
}
func (q *fakeQuerier) DeleteParameterValueByID(_ context.Context, id uuid.UUID) error {
q.mutex.Lock()
defer q.mutex.Unlock()
for index, parameterValue := range q.parameterValue {
if parameterValue.ID.String() != id.String() {
continue
}
q.parameterValue[index] = q.parameterValue[len(q.parameterValue)-1]
q.parameterValue = q.parameterValue[:len(q.parameterValue)-1]
return nil
}
return sql.ErrNoRows
}
func (q *fakeQuerier) GetAPIKeyByID(_ context.Context, id string) (database.APIKey, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
@ -210,41 +225,53 @@ func (q *fakeQuerier) GetWorkspaceOwnerCountsByProjectIDs(_ context.Context, pro
return res, nil
}
func (q *fakeQuerier) GetWorkspaceHistoryByID(_ context.Context, id uuid.UUID) (database.WorkspaceHistory, error) {
func (q *fakeQuerier) GetWorkspaceBuildByID(_ context.Context, id uuid.UUID) (database.WorkspaceBuild, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
for _, history := range q.workspaceHistory {
for _, history := range q.workspaceBuild {
if history.ID.String() == id.String() {
return history, nil
}
}
return database.WorkspaceHistory{}, sql.ErrNoRows
return database.WorkspaceBuild{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetWorkspaceHistoryByWorkspaceIDWithoutAfter(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceHistory, error) {
func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUID) (database.WorkspaceBuild, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
for _, workspaceHistory := range q.workspaceHistory {
if workspaceHistory.WorkspaceID.String() != workspaceID.String() {
continue
}
if !workspaceHistory.AfterID.Valid {
return workspaceHistory, nil
for _, build := range q.workspaceBuild {
if build.JobID.String() == jobID.String() {
return build, nil
}
}
return database.WorkspaceHistory{}, sql.ErrNoRows
return database.WorkspaceBuild{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetWorkspaceHistoryByWorkspaceID(_ context.Context, workspaceID uuid.UUID) ([]database.WorkspaceHistory, error) {
func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDWithoutAfter(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
history := make([]database.WorkspaceHistory, 0)
for _, workspaceHistory := range q.workspaceHistory {
if workspaceHistory.WorkspaceID.String() == workspaceID.String() {
history = append(history, workspaceHistory)
for _, workspaceBuild := range q.workspaceBuild {
if workspaceBuild.WorkspaceID.String() != workspaceID.String() {
continue
}
if !workspaceBuild.AfterID.Valid {
return workspaceBuild, nil
}
}
return database.WorkspaceBuild{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceID(_ context.Context, workspaceID uuid.UUID) ([]database.WorkspaceBuild, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
history := make([]database.WorkspaceBuild, 0)
for _, workspaceBuild := range q.workspaceBuild {
if workspaceBuild.WorkspaceID.String() == workspaceID.String() {
history = append(history, workspaceBuild)
}
}
if len(history) == 0 {
@ -253,40 +280,20 @@ func (q *fakeQuerier) GetWorkspaceHistoryByWorkspaceID(_ context.Context, worksp
return history, nil
}
func (q *fakeQuerier) GetWorkspaceHistoryByWorkspaceIDAndName(_ context.Context, arg database.GetWorkspaceHistoryByWorkspaceIDAndNameParams) (database.WorkspaceHistory, error) {
func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndName(_ context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndNameParams) (database.WorkspaceBuild, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
for _, workspaceHistory := range q.workspaceHistory {
if workspaceHistory.WorkspaceID.String() != arg.WorkspaceID.String() {
for _, workspaceBuild := range q.workspaceBuild {
if workspaceBuild.WorkspaceID.String() != arg.WorkspaceID.String() {
continue
}
if !strings.EqualFold(workspaceHistory.Name, arg.Name) {
if !strings.EqualFold(workspaceBuild.Name, arg.Name) {
continue
}
return workspaceHistory, nil
return workspaceBuild, nil
}
return database.WorkspaceHistory{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetWorkspacesByProjectAndUserID(_ context.Context, arg database.GetWorkspacesByProjectAndUserIDParams) ([]database.Workspace, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
workspaces := make([]database.Workspace, 0)
for _, workspace := range q.workspace {
if workspace.OwnerID != arg.OwnerID {
continue
}
if workspace.ProjectID.String() != arg.ProjectID.String() {
continue
}
workspaces = append(workspaces, workspace)
}
if len(workspaces) == 0 {
return nil, sql.ErrNoRows
}
return workspaces, nil
return database.WorkspaceBuild{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetWorkspacesByUserID(_ context.Context, ownerID string) ([]database.Workspace, error) {
@ -406,7 +413,7 @@ func (q *fakeQuerier) GetProjectVersionsByProjectID(_ context.Context, projectID
version := make([]database.ProjectVersion, 0)
for _, projectVersion := range q.projectVersion {
if projectVersion.ProjectID.String() != projectID.String() {
if projectVersion.ProjectID.UUID.String() != projectID.String() {
continue
}
version = append(version, projectVersion)
@ -422,7 +429,7 @@ func (q *fakeQuerier) GetProjectVersionByProjectIDAndName(_ context.Context, arg
defer q.mutex.Unlock()
for _, projectVersion := range q.projectVersion {
if projectVersion.ProjectID.String() != arg.ProjectID.String() {
if projectVersion.ProjectID.UUID.String() != arg.ProjectID.UUID.String() {
continue
}
if !strings.EqualFold(projectVersion.Name, arg.Name) {
@ -463,17 +470,34 @@ func (q *fakeQuerier) GetParameterSchemasByJobID(_ context.Context, jobID uuid.U
return parameters, nil
}
func (q *fakeQuerier) GetProjectsByOrganizationIDs(_ context.Context, ids []string) ([]database.Project, error) {
func (q *fakeQuerier) GetParameterValueByScopeAndName(_ context.Context, arg database.GetParameterValueByScopeAndNameParams) (database.ParameterValue, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
for _, parameterValue := range q.parameterValue {
if parameterValue.Scope != arg.Scope {
continue
}
if parameterValue.ScopeID != arg.ScopeID {
continue
}
if parameterValue.Name != arg.Name {
continue
}
return parameterValue, nil
}
return database.ParameterValue{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetProjectsByOrganization(_ context.Context, organizationID string) ([]database.Project, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
projects := make([]database.Project, 0)
for _, project := range q.project {
for _, id := range ids {
if project.OrganizationID == id {
projects = append(projects, project)
break
}
if project.OrganizationID == organizationID {
projects = append(projects, project)
break
}
}
if len(projects) == 0 {
@ -508,7 +532,19 @@ func (q *fakeQuerier) GetProvisionerDaemons(_ context.Context) ([]database.Provi
return q.provisionerDaemons, nil
}
func (q *fakeQuerier) GetProvisionerJobAgentByInstanceID(_ context.Context, instanceID string) (database.ProvisionerJobAgent, error) {
func (q *fakeQuerier) GetWorkspaceAgentByAuthToken(_ context.Context, authToken uuid.UUID) (database.WorkspaceAgent, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
for _, agent := range q.provisionerJobAgent {
if agent.AuthToken.String() == authToken.String() {
return agent, nil
}
}
return database.WorkspaceAgent{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetWorkspaceAgentByInstanceID(_ context.Context, instanceID string) (database.WorkspaceAgent, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
@ -519,25 +555,19 @@ func (q *fakeQuerier) GetProvisionerJobAgentByInstanceID(_ context.Context, inst
return agent, nil
}
}
return database.ProvisionerJobAgent{}, sql.ErrNoRows
return database.WorkspaceAgent{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetProvisionerJobAgentsByResourceIDs(_ context.Context, ids []uuid.UUID) ([]database.ProvisionerJobAgent, error) {
func (q *fakeQuerier) GetWorkspaceAgentByResourceID(_ context.Context, resourceID uuid.UUID) (database.WorkspaceAgent, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
agents := make([]database.ProvisionerJobAgent, 0)
for _, agent := range q.provisionerJobAgent {
for _, id := range ids {
if agent.ResourceID.String() == id.String() {
agents = append(agents, agent)
}
if agent.ResourceID.String() == resourceID.String() {
return agent, nil
}
}
if len(agents) == 0 {
return nil, sql.ErrNoRows
}
return agents, nil
return database.WorkspaceAgent{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetProvisionerDaemonByID(_ context.Context, id uuid.UUID) (database.ProvisionerDaemon, error) {
@ -566,7 +596,7 @@ func (q *fakeQuerier) GetProvisionerJobByID(_ context.Context, id uuid.UUID) (da
return database.ProvisionerJob{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetProvisionerJobResourceByID(_ context.Context, id uuid.UUID) (database.ProvisionerJobResource, error) {
func (q *fakeQuerier) GetWorkspaceResourceByID(_ context.Context, id uuid.UUID) (database.WorkspaceResource, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
@ -575,14 +605,14 @@ func (q *fakeQuerier) GetProvisionerJobResourceByID(_ context.Context, id uuid.U
return resource, nil
}
}
return database.ProvisionerJobResource{}, sql.ErrNoRows
return database.WorkspaceResource{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetProvisionerJobResourcesByJobID(_ context.Context, jobID uuid.UUID) ([]database.ProvisionerJobResource, error) {
func (q *fakeQuerier) GetWorkspaceResourcesByJobID(_ context.Context, jobID uuid.UUID) ([]database.WorkspaceResource, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
resources := make([]database.ProvisionerJobResource, 0)
resources := make([]database.WorkspaceResource, 0)
for _, resource := range q.provisionerJobResource {
if resource.JobID.String() != jobID.String() {
continue
@ -595,6 +625,26 @@ func (q *fakeQuerier) GetProvisionerJobResourcesByJobID(_ context.Context, jobID
return resources, nil
}
func (q *fakeQuerier) GetProvisionerJobsByIDs(_ context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
jobs := make([]database.ProvisionerJob, 0)
for _, job := range q.provisionerJobs {
for _, id := range ids {
if id.String() == job.ID.String() {
jobs = append(jobs, job)
break
}
}
}
if len(jobs) == 0 {
return nil, sql.ErrNoRows
}
return jobs, nil
}
func (q *fakeQuerier) GetProvisionerLogsByIDBetween(_ context.Context, arg database.GetProvisionerLogsByIDBetweenParams) ([]database.ProvisionerJobLog, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
@ -734,13 +784,14 @@ func (q *fakeQuerier) InsertProjectVersion(_ context.Context, arg database.Inser
//nolint:gosimple
version := database.ProjectVersion{
ID: arg.ID,
ProjectID: arg.ProjectID,
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
Name: arg.Name,
Description: arg.Description,
ImportJobID: arg.ImportJobID,
ID: arg.ID,
ProjectID: arg.ProjectID,
OrganizationID: arg.OrganizationID,
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
Name: arg.Name,
Description: arg.Description,
JobID: arg.JobID,
}
q.projectVersion = append(q.projectVersion, version)
return version, nil
@ -797,10 +848,11 @@ func (q *fakeQuerier) InsertProvisionerDaemon(_ context.Context, arg database.In
defer q.mutex.Unlock()
daemon := database.ProvisionerDaemon{
ID: arg.ID,
CreatedAt: arg.CreatedAt,
Name: arg.Name,
Provisioners: arg.Provisioners,
ID: arg.ID,
CreatedAt: arg.CreatedAt,
OrganizationID: arg.OrganizationID,
Name: arg.Name,
Provisioners: arg.Provisioners,
}
q.provisionerDaemons = append(q.provisionerDaemons, daemon)
return daemon, nil
@ -826,12 +878,12 @@ func (q *fakeQuerier) InsertProvisionerJob(_ context.Context, arg database.Inser
return job, nil
}
func (q *fakeQuerier) InsertProvisionerJobAgent(_ context.Context, arg database.InsertProvisionerJobAgentParams) (database.ProvisionerJobAgent, error) {
func (q *fakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.InsertWorkspaceAgentParams) (database.WorkspaceAgent, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
//nolint:gosimple
agent := database.ProvisionerJobAgent{
agent := database.WorkspaceAgent{
ID: arg.ID,
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
@ -847,12 +899,12 @@ func (q *fakeQuerier) InsertProvisionerJobAgent(_ context.Context, arg database.
return agent, nil
}
func (q *fakeQuerier) InsertProvisionerJobResource(_ context.Context, arg database.InsertProvisionerJobResourceParams) (database.ProvisionerJobResource, error) {
func (q *fakeQuerier) InsertWorkspaceResource(_ context.Context, arg database.InsertWorkspaceResourceParams) (database.WorkspaceResource, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
//nolint:gosimple
resource := database.ProvisionerJobResource{
resource := database.WorkspaceResource{
ID: arg.ID,
CreatedAt: arg.CreatedAt,
JobID: arg.JobID,
@ -900,11 +952,11 @@ func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWork
return workspace, nil
}
func (q *fakeQuerier) InsertWorkspaceHistory(_ context.Context, arg database.InsertWorkspaceHistoryParams) (database.WorkspaceHistory, error) {
func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.InsertWorkspaceBuildParams) (database.WorkspaceBuild, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
workspaceHistory := database.WorkspaceHistory{
workspaceBuild := database.WorkspaceBuild{
ID: arg.ID,
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
@ -914,11 +966,11 @@ func (q *fakeQuerier) InsertWorkspaceHistory(_ context.Context, arg database.Ins
BeforeID: arg.BeforeID,
Transition: arg.Transition,
Initiator: arg.Initiator,
ProvisionJobID: arg.ProvisionJobID,
JobID: arg.JobID,
ProvisionerState: arg.ProvisionerState,
}
q.workspaceHistory = append(q.workspaceHistory, workspaceHistory)
return workspaceHistory, nil
q.workspaceBuild = append(q.workspaceBuild, workspaceBuild)
return workspaceBuild, nil
}
func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPIKeyByIDParams) error {
@ -940,6 +992,22 @@ func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPI
return sql.ErrNoRows
}
func (q *fakeQuerier) UpdateProjectVersionByID(_ context.Context, arg database.UpdateProjectVersionByIDParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()
for index, projectVersion := range q.projectVersion {
if projectVersion.ID.String() != arg.ID.String() {
continue
}
projectVersion.ProjectID = arg.ProjectID
projectVersion.UpdatedAt = arg.UpdatedAt
q.projectVersion[index] = projectVersion
return nil
}
return sql.ErrNoRows
}
func (q *fakeQuerier) UpdateProvisionerDaemonByID(_ context.Context, arg database.UpdateProvisionerDaemonByIDParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()
@ -956,6 +1024,21 @@ func (q *fakeQuerier) UpdateProvisionerDaemonByID(_ context.Context, arg databas
return sql.ErrNoRows
}
func (q *fakeQuerier) UpdateWorkspaceAgentByID(_ context.Context, arg database.UpdateWorkspaceAgentByIDParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()
for index, agent := range q.provisionerJobAgent {
if agent.ID.String() != arg.ID.String() {
continue
}
agent.UpdatedAt = arg.UpdatedAt
q.provisionerJobAgent[index] = agent
return nil
}
return sql.ErrNoRows
}
func (q *fakeQuerier) UpdateProvisionerJobByID(_ context.Context, arg database.UpdateProvisionerJobByIDParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()
@ -989,18 +1072,18 @@ func (q *fakeQuerier) UpdateProvisionerJobWithCompleteByID(_ context.Context, ar
return sql.ErrNoRows
}
func (q *fakeQuerier) UpdateWorkspaceHistoryByID(_ context.Context, arg database.UpdateWorkspaceHistoryByIDParams) error {
func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.UpdateWorkspaceBuildByIDParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()
for index, workspaceHistory := range q.workspaceHistory {
if workspaceHistory.ID.String() != arg.ID.String() {
for index, workspaceBuild := range q.workspaceBuild {
if workspaceBuild.ID.String() != arg.ID.String() {
continue
}
workspaceHistory.UpdatedAt = arg.UpdatedAt
workspaceHistory.AfterID = arg.AfterID
workspaceHistory.ProvisionerState = arg.ProvisionerState
q.workspaceHistory[index] = workspaceHistory
workspaceBuild.UpdatedAt = arg.UpdatedAt
workspaceBuild.AfterID = arg.AfterID
workspaceBuild.ProvisionerState = arg.ProvisionerState
q.workspaceBuild[index] = workspaceBuild
return nil
}
return sql.ErrNoRows

110
database/dump.sql generated
View File

@ -45,7 +45,7 @@ CREATE TYPE parameter_type_system AS ENUM (
CREATE TYPE provisioner_job_type AS ENUM (
'project_version_import',
'workspace_provision'
'workspace_build'
);
CREATE TYPE provisioner_storage_method AS ENUM (
@ -143,11 +143,11 @@ CREATE TABLE parameter_schema (
CREATE TABLE parameter_value (
id uuid NOT NULL,
name character varying(64) NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
scope parameter_scope NOT NULL,
scope_id text NOT NULL,
name character varying(64) NOT NULL,
source_scheme parameter_source_scheme NOT NULL,
source_value text NOT NULL,
destination_scheme parameter_destination_scheme NOT NULL
@ -165,18 +165,20 @@ CREATE TABLE project (
CREATE TABLE project_version (
id uuid NOT NULL,
project_id uuid NOT NULL,
project_id uuid,
organization_id text NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
name character varying(64) NOT NULL,
description character varying(1048576) NOT NULL,
import_job_id uuid NOT NULL
job_id uuid NOT NULL
);
CREATE TABLE provisioner_daemon (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone,
organization_id text,
name character varying(64) NOT NULL,
provisioners provisioner_type[] NOT NULL
);
@ -199,19 +201,6 @@ CREATE TABLE provisioner_job (
worker_id uuid
);
CREATE TABLE provisioner_job_agent (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone,
resource_id uuid NOT NULL,
auth_token uuid NOT NULL,
auth_instance_id character varying(64),
environment_variables jsonb,
startup_script character varying(65534),
instance_metadata jsonb,
resource_metadata jsonb
);
CREATE TABLE provisioner_job_log (
id uuid NOT NULL,
job_id uuid NOT NULL,
@ -221,16 +210,6 @@ CREATE TABLE provisioner_job_log (
output character varying(1024) NOT NULL
);
CREATE TABLE provisioner_job_resource (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
job_id uuid NOT NULL,
transition workspace_transition NOT NULL,
type character varying(256) NOT NULL,
name character varying(64) NOT NULL,
agent_id uuid
);
CREATE TABLE users (
id text NOT NULL,
email text NOT NULL,
@ -262,7 +241,20 @@ CREATE TABLE workspace (
name character varying(64) NOT NULL
);
CREATE TABLE workspace_history (
CREATE TABLE workspace_agent (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone,
resource_id uuid NOT NULL,
auth_token uuid NOT NULL,
auth_instance_id character varying(64),
environment_variables jsonb,
startup_script character varying(65534),
instance_metadata jsonb,
resource_metadata jsonb
);
CREATE TABLE workspace_build (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
@ -274,7 +266,17 @@ CREATE TABLE workspace_history (
transition workspace_transition NOT NULL,
initiator character varying(255) NOT NULL,
provisioner_state bytea,
provision_job_id uuid NOT NULL
job_id uuid NOT NULL
);
CREATE TABLE workspace_resource (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
job_id uuid NOT NULL,
transition workspace_transition NOT NULL,
type character varying(256) NOT NULL,
name character varying(64) NOT NULL,
agent_id uuid
);
ALTER TABLE ONLY file
@ -290,7 +292,7 @@ ALTER TABLE ONLY parameter_value
ADD CONSTRAINT parameter_value_id_key UNIQUE (id);
ALTER TABLE ONLY parameter_value
ADD CONSTRAINT parameter_value_name_scope_scope_id_key UNIQUE (name, scope, scope_id);
ADD CONSTRAINT parameter_value_scope_id_name_key UNIQUE (scope_id, name);
ALTER TABLE ONLY project
ADD CONSTRAINT project_id_key UNIQUE (id);
@ -310,26 +312,26 @@ ALTER TABLE ONLY provisioner_daemon
ALTER TABLE ONLY provisioner_daemon
ADD CONSTRAINT provisioner_daemon_name_key UNIQUE (name);
ALTER TABLE ONLY provisioner_job_agent
ADD CONSTRAINT provisioner_job_agent_auth_token_key UNIQUE (auth_token);
ALTER TABLE ONLY provisioner_job_agent
ADD CONSTRAINT provisioner_job_agent_id_key UNIQUE (id);
ALTER TABLE ONLY provisioner_job
ADD CONSTRAINT provisioner_job_id_key UNIQUE (id);
ALTER TABLE ONLY provisioner_job_log
ADD CONSTRAINT provisioner_job_log_id_key UNIQUE (id);
ALTER TABLE ONLY provisioner_job_resource
ADD CONSTRAINT provisioner_job_resource_id_key UNIQUE (id);
ALTER TABLE ONLY workspace_agent
ADD CONSTRAINT workspace_agent_auth_token_key UNIQUE (auth_token);
ALTER TABLE ONLY workspace_history
ADD CONSTRAINT workspace_history_id_key UNIQUE (id);
ALTER TABLE ONLY workspace_agent
ADD CONSTRAINT workspace_agent_id_key UNIQUE (id);
ALTER TABLE ONLY workspace_history
ADD CONSTRAINT workspace_history_workspace_id_name_key UNIQUE (workspace_id, name);
ALTER TABLE ONLY workspace_build
ADD CONSTRAINT workspace_build_id_key UNIQUE (id);
ALTER TABLE ONLY workspace_build
ADD CONSTRAINT workspace_build_job_id_key UNIQUE (job_id);
ALTER TABLE ONLY workspace_build
ADD CONSTRAINT workspace_build_workspace_id_name_key UNIQUE (workspace_id, name);
ALTER TABLE ONLY workspace
ADD CONSTRAINT workspace_id_key UNIQUE (id);
@ -337,27 +339,33 @@ ALTER TABLE ONLY workspace
ALTER TABLE ONLY workspace
ADD CONSTRAINT workspace_owner_id_name_key UNIQUE (owner_id, name);
ALTER TABLE ONLY workspace_resource
ADD CONSTRAINT workspace_resource_id_key UNIQUE (id);
ALTER TABLE ONLY parameter_schema
ADD CONSTRAINT parameter_schema_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_job(id) ON DELETE CASCADE;
ALTER TABLE ONLY project_version
ADD CONSTRAINT project_version_project_id_fkey FOREIGN KEY (project_id) REFERENCES project(id);
ALTER TABLE ONLY provisioner_job_agent
ADD CONSTRAINT provisioner_job_agent_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES provisioner_job_resource(id) ON DELETE CASCADE;
ALTER TABLE ONLY provisioner_job_log
ADD CONSTRAINT provisioner_job_log_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_job(id) ON DELETE CASCADE;
ALTER TABLE ONLY provisioner_job_resource
ADD CONSTRAINT provisioner_job_resource_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_job(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_agent
ADD CONSTRAINT workspace_agent_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resource(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_history
ADD CONSTRAINT workspace_history_project_version_id_fkey FOREIGN KEY (project_version_id) REFERENCES project_version(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_build
ADD CONSTRAINT workspace_build_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_job(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_history
ADD CONSTRAINT workspace_history_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_build
ADD CONSTRAINT workspace_build_project_version_id_fkey FOREIGN KEY (project_version_id) REFERENCES project_version(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_build
ADD CONSTRAINT workspace_build_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace
ADD CONSTRAINT workspace_project_id_fkey FOREIGN KEY (project_id) REFERENCES project(id);
ALTER TABLE ONLY workspace_resource
ADD CONSTRAINT workspace_resource_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_job(id) ON DELETE CASCADE;

View File

@ -33,7 +33,8 @@ CREATE TABLE project (
CREATE TABLE project_version (
id uuid NOT NULL UNIQUE,
-- This should be indexed.
project_id uuid NOT NULL REFERENCES project (id),
project_id uuid REFERENCES project (id),
organization_id text NOT NULL,
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL,
-- Name is generated for ease of differentiation.
@ -42,9 +43,8 @@ CREATE TABLE project_version (
-- Extracted from a README.md on import.
-- Maximum of 1MB.
description varchar(1048576) NOT NULL,
-- The import job for a Project Version. This is used
-- to detect if an import was successful.
import_job_id uuid NOT NULL,
-- The job ID for building the project version.
job_id uuid NOT NULL,
-- Disallow projects to have the same build name
-- multiple times.
UNIQUE(project_id, name)

View File

@ -14,21 +14,3 @@ CREATE TYPE workspace_transition AS ENUM (
'delete'
);
-- Workspace transition represents a change in workspace state.
CREATE TABLE workspace_history (
id uuid NOT NULL UNIQUE,
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL,
workspace_id uuid NOT NULL REFERENCES workspace (id) ON DELETE CASCADE,
project_version_id uuid NOT NULL REFERENCES project_version (id) ON DELETE CASCADE,
name varchar(64) NOT NULL,
before_id uuid,
after_id uuid,
transition workspace_transition NOT NULL,
initiator varchar(255) NOT NULL,
-- State stored by the provisioner
provisioner_state bytea,
-- Job ID of the action
provision_job_id uuid NOT NULL,
UNIQUE(workspace_id, name)
);

View File

@ -2,6 +2,7 @@ CREATE TABLE IF NOT EXISTS provisioner_daemon (
id uuid NOT NULL UNIQUE,
created_at timestamptz NOT NULL,
updated_at timestamptz,
organization_id text,
-- Name is generated for ease of differentiation.
-- eg. WowBananas16
name varchar(64) NOT NULL UNIQUE,
@ -10,7 +11,7 @@ CREATE TABLE IF NOT EXISTS provisioner_daemon (
CREATE TYPE provisioner_job_type AS ENUM (
'project_version_import',
'workspace_provision'
'workspace_build'
);
CREATE TYPE provisioner_storage_method AS ENUM ('file');
@ -55,8 +56,7 @@ CREATE TABLE IF NOT EXISTS provisioner_job_log (
output varchar(1024) NOT NULL
);
-- Resources from multiple workspace states are stored here post project-import job.
CREATE TABLE provisioner_job_resource (
CREATE TABLE workspace_resource (
id uuid NOT NULL UNIQUE,
created_at timestamptz NOT NULL,
job_id uuid NOT NULL REFERENCES provisioner_job(id) ON DELETE CASCADE,
@ -66,12 +66,11 @@ CREATE TABLE provisioner_job_resource (
agent_id uuid
);
-- Agents that associate with a specific resource.
CREATE TABLE provisioner_job_agent (
CREATE TABLE workspace_agent (
id uuid NOT NULL UNIQUE,
created_at timestamptz NOT NULL,
updated_at timestamptz,
resource_id uuid NOT NULL REFERENCES provisioner_job_resource (id) ON DELETE CASCADE,
resource_id uuid NOT NULL REFERENCES workspace_resource (id) ON DELETE CASCADE,
auth_token uuid NOT NULL UNIQUE,
auth_instance_id varchar(64),
environment_variables jsonb,
@ -133,14 +132,32 @@ CREATE TABLE parameter_schema (
-- Parameters are provided to jobs for provisioning and to workspaces.
CREATE TABLE parameter_value (
id uuid NOT NULL UNIQUE,
name varchar(64) NOT NULL,
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL,
scope parameter_scope NOT NULL,
scope_id text NOT NULL,
name varchar(64) NOT NULL,
source_scheme parameter_source_scheme NOT NULL,
source_value text NOT NULL,
destination_scheme parameter_destination_scheme NOT NULL,
-- Prevents duplicates for parameters in the same scope.
UNIQUE(name, scope, scope_id)
UNIQUE(scope_id, name)
);
CREATE TABLE workspace_build (
id uuid NOT NULL UNIQUE,
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL,
workspace_id uuid NOT NULL REFERENCES workspace (id) ON DELETE CASCADE,
project_version_id uuid NOT NULL REFERENCES project_version (id) ON DELETE CASCADE,
name varchar(64) NOT NULL,
before_id uuid,
after_id uuid,
transition workspace_transition NOT NULL,
initiator varchar(255) NOT NULL,
-- State stored by the provisioner
provisioner_state bytea,
-- Job ID of the action
job_id uuid NOT NULL UNIQUE REFERENCES provisioner_job(id) ON DELETE CASCADE,
UNIQUE(workspace_id, name)
);

View File

@ -157,7 +157,7 @@ type ProvisionerJobType string
const (
ProvisionerJobTypeProjectVersionImport ProvisionerJobType = "project_version_import"
ProvisionerJobTypeWorkspaceProvision ProvisionerJobType = "workspace_provision"
ProvisionerJobTypeWorkspaceBuild ProvisionerJobType = "workspace_build"
)
func (e *ProvisionerJobType) Scan(src interface{}) error {
@ -323,11 +323,11 @@ type ParameterSchema struct {
type ParameterValue struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
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 ParameterSourceScheme `db:"source_scheme" json:"source_scheme"`
SourceValue string `db:"source_value" json:"source_value"`
DestinationScheme ParameterDestinationScheme `db:"destination_scheme" json:"destination_scheme"`
@ -344,21 +344,23 @@ type Project struct {
}
type ProjectVersion struct {
ID uuid.UUID `db:"id" json:"id"`
ProjectID uuid.UUID `db:"project_id" json:"project_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
Description string `db:"description" json:"description"`
ImportJobID uuid.UUID `db:"import_job_id" json:"import_job_id"`
ID uuid.UUID `db:"id" json:"id"`
ProjectID uuid.NullUUID `db:"project_id" json:"project_id"`
OrganizationID string `db:"organization_id" json:"organization_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
Description string `db:"description" json:"description"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
}
type ProvisionerDaemon struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
Provisioners []ProvisionerType `db:"provisioners" json:"provisioners"`
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"`
OrganizationID sql.NullString `db:"organization_id" json:"organization_id"`
Name string `db:"name" json:"name"`
Provisioners []ProvisionerType `db:"provisioners" json:"provisioners"`
}
type ProvisionerJob struct {
@ -379,19 +381,6 @@ type ProvisionerJob struct {
WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"`
}
type ProvisionerJobAgent struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"`
ResourceID uuid.UUID `db:"resource_id" json:"resource_id"`
AuthToken uuid.UUID `db:"auth_token" json:"auth_token"`
AuthInstanceID sql.NullString `db:"auth_instance_id" json:"auth_instance_id"`
EnvironmentVariables pqtype.NullRawMessage `db:"environment_variables" json:"environment_variables"`
StartupScript sql.NullString `db:"startup_script" json:"startup_script"`
InstanceMetadata pqtype.NullRawMessage `db:"instance_metadata" json:"instance_metadata"`
ResourceMetadata pqtype.NullRawMessage `db:"resource_metadata" json:"resource_metadata"`
}
type ProvisionerJobLog struct {
ID uuid.UUID `db:"id" json:"id"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
@ -401,16 +390,6 @@ type ProvisionerJobLog struct {
Output string `db:"output" json:"output"`
}
type ProvisionerJobResource struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
Transition WorkspaceTransition `db:"transition" json:"transition"`
Type string `db:"type" json:"type"`
Name string `db:"name" json:"name"`
AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"`
}
type User struct {
ID string `db:"id" json:"id"`
Email string `db:"email" json:"email"`
@ -442,7 +421,20 @@ type Workspace struct {
Name string `db:"name" json:"name"`
}
type WorkspaceHistory struct {
type WorkspaceAgent struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"`
ResourceID uuid.UUID `db:"resource_id" json:"resource_id"`
AuthToken uuid.UUID `db:"auth_token" json:"auth_token"`
AuthInstanceID sql.NullString `db:"auth_instance_id" json:"auth_instance_id"`
EnvironmentVariables pqtype.NullRawMessage `db:"environment_variables" json:"environment_variables"`
StartupScript sql.NullString `db:"startup_script" json:"startup_script"`
InstanceMetadata pqtype.NullRawMessage `db:"instance_metadata" json:"instance_metadata"`
ResourceMetadata pqtype.NullRawMessage `db:"resource_metadata" json:"resource_metadata"`
}
type WorkspaceBuild 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"`
@ -454,5 +446,15 @@ type WorkspaceHistory struct {
Transition WorkspaceTransition `db:"transition" json:"transition"`
Initiator string `db:"initiator" json:"initiator"`
ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"`
ProvisionJobID uuid.UUID `db:"provision_job_id" json:"provision_job_id"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
}
type WorkspaceResource struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
Transition WorkspaceTransition `db:"transition" json:"transition"`
Type string `db:"type" json:"type"`
Name string `db:"name" json:"name"`
AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"`
}

View File

@ -10,6 +10,7 @@ import (
type querier interface {
AcquireProvisionerJob(ctx context.Context, arg AcquireProvisionerJobParams) (ProvisionerJob, error)
DeleteParameterValueByID(ctx context.Context, id uuid.UUID) error
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
GetFileByHash(ctx context.Context, hash string) (File, error)
GetOrganizationByID(ctx context.Context, id string) (Organization, error)
@ -17,32 +18,35 @@ type querier interface {
GetOrganizationMemberByUserID(ctx context.Context, arg GetOrganizationMemberByUserIDParams) (OrganizationMember, error)
GetOrganizationsByUserID(ctx context.Context, userID string) ([]Organization, error)
GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error)
GetParameterValueByScopeAndName(ctx context.Context, arg GetParameterValueByScopeAndNameParams) (ParameterValue, error)
GetParameterValuesByScope(ctx context.Context, arg GetParameterValuesByScopeParams) ([]ParameterValue, error)
GetProjectByID(ctx context.Context, id uuid.UUID) (Project, error)
GetProjectByOrganizationAndName(ctx context.Context, arg GetProjectByOrganizationAndNameParams) (Project, error)
GetProjectVersionByID(ctx context.Context, id uuid.UUID) (ProjectVersion, error)
GetProjectVersionByProjectIDAndName(ctx context.Context, arg GetProjectVersionByProjectIDAndNameParams) (ProjectVersion, error)
GetProjectVersionsByProjectID(ctx context.Context, projectID uuid.UUID) ([]ProjectVersion, error)
GetProjectsByOrganizationIDs(ctx context.Context, ids []string) ([]Project, error)
GetProjectVersionsByProjectID(ctx context.Context, dollar_1 uuid.UUID) ([]ProjectVersion, error)
GetProjectsByOrganization(ctx context.Context, organizationID string) ([]Project, error)
GetProvisionerDaemonByID(ctx context.Context, id uuid.UUID) (ProvisionerDaemon, error)
GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error)
GetProvisionerJobAgentByInstanceID(ctx context.Context, authInstanceID string) (ProvisionerJobAgent, error)
GetProvisionerJobAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJobAgent, error)
GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error)
GetProvisionerJobResourceByID(ctx context.Context, id uuid.UUID) (ProvisionerJobResource, error)
GetProvisionerJobResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]ProvisionerJobResource, error)
GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJob, error)
GetProvisionerLogsByIDBetween(ctx context.Context, arg GetProvisionerLogsByIDBetweenParams) ([]ProvisionerJobLog, error)
GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error)
GetUserByID(ctx context.Context, id string) (User, error)
GetUserCount(ctx context.Context) (int64, error)
GetWorkspaceAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (WorkspaceAgent, error)
GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error)
GetWorkspaceAgentByResourceID(ctx context.Context, resourceID uuid.UUID) (WorkspaceAgent, error)
GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error)
GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error)
GetWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceBuild, error)
GetWorkspaceBuildByWorkspaceIDAndName(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndNameParams) (WorkspaceBuild, error)
GetWorkspaceBuildByWorkspaceIDWithoutAfter(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error)
GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error)
GetWorkspaceByUserIDAndName(ctx context.Context, arg GetWorkspaceByUserIDAndNameParams) (Workspace, error)
GetWorkspaceHistoryByID(ctx context.Context, id uuid.UUID) (WorkspaceHistory, error)
GetWorkspaceHistoryByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceHistory, error)
GetWorkspaceHistoryByWorkspaceIDAndName(ctx context.Context, arg GetWorkspaceHistoryByWorkspaceIDAndNameParams) (WorkspaceHistory, error)
GetWorkspaceHistoryByWorkspaceIDWithoutAfter(ctx context.Context, workspaceID uuid.UUID) (WorkspaceHistory, error)
GetWorkspaceOwnerCountsByProjectIDs(ctx context.Context, ids []uuid.UUID) ([]GetWorkspaceOwnerCountsByProjectIDsRow, error)
GetWorkspacesByProjectAndUserID(ctx context.Context, arg GetWorkspacesByProjectAndUserIDParams) ([]Workspace, error)
GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error)
GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error)
GetWorkspacesByUserID(ctx context.Context, ownerID string) ([]Workspace, error)
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
InsertFile(ctx context.Context, arg InsertFileParams) (File, error)
@ -54,17 +58,19 @@ type querier interface {
InsertProjectVersion(ctx context.Context, arg InsertProjectVersionParams) (ProjectVersion, error)
InsertProvisionerDaemon(ctx context.Context, arg InsertProvisionerDaemonParams) (ProvisionerDaemon, error)
InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error)
InsertProvisionerJobAgent(ctx context.Context, arg InsertProvisionerJobAgentParams) (ProvisionerJobAgent, error)
InsertProvisionerJobLogs(ctx context.Context, arg InsertProvisionerJobLogsParams) ([]ProvisionerJobLog, error)
InsertProvisionerJobResource(ctx context.Context, arg InsertProvisionerJobResourceParams) (ProvisionerJobResource, error)
InsertUser(ctx context.Context, arg InsertUserParams) (User, error)
InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error)
InsertWorkspaceHistory(ctx context.Context, arg InsertWorkspaceHistoryParams) (WorkspaceHistory, error)
InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error)
InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuild, error)
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error)
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
UpdateProjectVersionByID(ctx context.Context, arg UpdateProjectVersionByIDParams) error
UpdateProvisionerDaemonByID(ctx context.Context, arg UpdateProvisionerDaemonByIDParams) error
UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error
UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error
UpdateWorkspaceHistoryByID(ctx context.Context, arg UpdateWorkspaceHistoryByIDParams) error
UpdateWorkspaceAgentByID(ctx context.Context, arg UpdateWorkspaceAgentByIDParams) error
UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) error
}
var _ querier = (*sqlQuerier)(nil)

View File

@ -36,6 +36,12 @@ WHERE
1
) RETURNING *;
-- name: DeleteParameterValueByID :exec
DELETE FROM
parameter_value
WHERE
id = $1;
-- name: GetAPIKeyByID :one
SELECT
*
@ -136,6 +142,18 @@ WHERE
scope = $1
AND scope_id = $2;
-- name: GetParameterValueByScopeAndName :one
SELECT
*
FROM
parameter_value
WHERE
scope = $1
AND scope_id = $2
AND name = $3
LIMIT
1;
-- name: GetProjectByID :one
SELECT
*
@ -157,13 +175,13 @@ WHERE
LIMIT
1;
-- name: GetProjectsByOrganizationIDs :many
-- name: GetProjectsByOrganization :many
SELECT
*
FROM
project
WHERE
organization_id = ANY(@ids :: text [ ]);
organization_id = $1;
-- name: GetParameterSchemasByJobID :many
SELECT
@ -179,7 +197,7 @@ SELECT
FROM
project_version
WHERE
project_id = $1;
project_id = $1 :: uuid;
-- name: GetProjectVersionByProjectIDAndName :one
SELECT
@ -226,11 +244,19 @@ SELECT
FROM
provisioner_daemon;
-- name: GetProvisionerJobAgentByInstanceID :one
-- name: GetWorkspaceAgentByAuthToken :one
SELECT
*
FROM
provisioner_job_agent
workspace_agent
WHERE
auth_token = $1;
-- name: GetWorkspaceAgentByInstanceID :one
SELECT
*
FROM
workspace_agent
WHERE
auth_instance_id = @auth_instance_id :: text
ORDER BY
@ -244,6 +270,14 @@ FROM
WHERE
id = $1;
-- name: GetProvisionerJobsByIDs :many
SELECT
*
FROM
provisioner_job
WHERE
id = ANY(@ids :: uuid [ ]);
-- name: GetWorkspaceByID :one
SELECT
*
@ -271,15 +305,6 @@ WHERE
owner_id = @owner_id
AND LOWER(name) = LOWER(@name);
-- name: GetWorkspacesByProjectAndUserID :many
SELECT
*
FROM
workspace
WHERE
owner_id = $1
AND project_id = $2;
-- name: GetWorkspaceOwnerCountsByProjectIDs :many
SELECT
project_id,
@ -292,67 +317,77 @@ GROUP BY
project_id,
owner_id;
-- name: GetWorkspaceHistoryByID :one
-- name: GetWorkspaceBuildByID :one
SELECT
*
FROM
workspace_history
workspace_build
WHERE
id = $1
LIMIT
1;
-- name: GetWorkspaceHistoryByWorkspaceIDAndName :one
-- name: GetWorkspaceBuildByJobID :one
SELECT
*
FROM
workspace_history
workspace_build
WHERE
job_id = $1
LIMIT
1;
-- name: GetWorkspaceBuildByWorkspaceIDAndName :one
SELECT
*
FROM
workspace_build
WHERE
workspace_id = $1
AND name = $2;
-- name: GetWorkspaceHistoryByWorkspaceID :many
-- name: GetWorkspaceBuildByWorkspaceID :many
SELECT
*
FROM
workspace_history
workspace_build
WHERE
workspace_id = $1;
-- name: GetWorkspaceHistoryByWorkspaceIDWithoutAfter :one
-- name: GetWorkspaceBuildByWorkspaceIDWithoutAfter :one
SELECT
*
FROM
workspace_history
workspace_build
WHERE
workspace_id = $1
AND after_id IS NULL
LIMIT
1;
-- name: GetProvisionerJobResourceByID :one
-- name: GetWorkspaceResourceByID :one
SELECT
*
FROM
provisioner_job_resource
workspace_resource
WHERE
id = $1;
-- name: GetProvisionerJobResourcesByJobID :many
-- name: GetWorkspaceResourcesByJobID :many
SELECT
*
FROM
provisioner_job_resource
workspace_resource
WHERE
job_id = $1;
-- name: GetProvisionerJobAgentsByResourceIDs :many
-- name: GetWorkspaceAgentByResourceID :one
SELECT
*
FROM
provisioner_job_agent
workspace_agent
WHERE
resource_id = ANY(@ids :: uuid [ ]);
resource_id = $1;
-- name: InsertAPIKey :one
INSERT INTO
@ -457,9 +492,9 @@ INSERT INTO
VALUES
($1, $2, $3, $4, $5, $6, $7) RETURNING *;
-- name: InsertProvisionerJobResource :one
-- name: InsertWorkspaceResource :one
INSERT INTO
provisioner_job_resource (
workspace_resource (
id,
created_at,
job_id,
@ -476,14 +511,15 @@ INSERT INTO
project_version (
id,
project_id,
organization_id,
created_at,
updated_at,
name,
description,
import_job_id
job_id
)
VALUES
($1, $2, $3, $4, $5, $6, $7) RETURNING *;
($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *;
-- name: InsertParameterSchema :one
INSERT INTO
@ -527,9 +563,9 @@ VALUES
-- name: InsertProvisionerDaemon :one
INSERT INTO
provisioner_daemon (id, created_at, name, provisioners)
provisioner_daemon (id, created_at, organization_id, name, provisioners)
VALUES
($1, $2, $3, $4) RETURNING *;
($1, $2, $3, $4, $5) RETURNING *;
-- name: InsertProvisionerJob :one
INSERT INTO
@ -577,9 +613,9 @@ INSERT INTO
VALUES
($1, $2, $3, $4, $5, $6) RETURNING *;
-- name: InsertProvisionerJobAgent :one
-- name: InsertWorkspaceAgent :one
INSERT INTO
provisioner_job_agent (
workspace_agent (
id,
created_at,
updated_at,
@ -594,9 +630,9 @@ INSERT INTO
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *;
-- name: InsertWorkspaceHistory :one
-- name: InsertWorkspaceBuild :one
INSERT INTO
workspace_history (
workspace_build (
id,
created_at,
updated_at,
@ -606,7 +642,7 @@ INSERT INTO
name,
transition,
initiator,
provision_job_id,
job_id,
provisioner_state
)
VALUES
@ -624,6 +660,15 @@ SET
WHERE
id = $1;
-- name: UpdateProjectVersionByID :exec
UPDATE
project_version
SET
project_id = $2,
updated_at = $3
WHERE
id = $1;
-- name: UpdateProvisionerDaemonByID :exec
UPDATE
provisioner_daemon
@ -652,9 +697,17 @@ SET
WHERE
id = $1;
-- name: UpdateWorkspaceHistoryByID :exec
-- name: UpdateWorkspaceAgentByID :exec
UPDATE
workspace_history
workspace_agent
SET
updated_at = $2
WHERE
id = $1;
-- name: UpdateWorkspaceBuildByID :exec
UPDATE
workspace_build
SET
updated_at = $2,
after_id = $3,

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@ function create_initial_user() {
curl -X POST \
-d "{\"email\": \"$EMAIL\", \"username\": \"$USERNAME\", \"organization\": \"$ORGANIZATION\", \"password\": \"$PASSWORD\"}" \
-H 'Content-Type:application/json' \
http://localhost:3000/api/v2/user
http://localhost:3000/api/v2/users/first
}
# Run yarn install, to make sure node_modules are ready to go

8
go.mod
View File

@ -14,11 +14,8 @@ replace github.com/hashicorp/terraform-config-inspect => github.com/kylecarbs/te
// Required until https://github.com/chzyer/readline/pull/198 is merged.
replace github.com/chzyer/readline => github.com/kylecarbs/readline v0.0.0-20220211054233-0d62993714c8
// Required until https://github.com/census-instrumentation/opencensus-go/pull/1272 is merged.
replace go.opencensus.io => github.com/kylecarbs/opencensus-go v0.23.1-0.20220220184033-4441763886a2
// Required until https://github.com/pion/ice/pull/425 is merged.
replace github.com/pion/ice/v2 => github.com/kylecarbs/ice/v2 v2.1.8-0.20220221162453-b262a62902c3
// opencensus-go leaks a goroutine by default.
replace go.opencensus.io => github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b
require (
cdr.dev/slog v1.4.1
@ -53,6 +50,7 @@ require (
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/quasilyte/go-ruleguard/dsl v0.3.17
github.com/spf13/cobra v1.3.0
github.com/stretchr/testify v1.7.0

31
go.sum
View File

@ -47,6 +47,7 @@ cloud.google.com/go/compute v1.5.0 h1:b1zWmYuuHz7gO9kDcM/EpHGr06UgsYNRpNJzI2kFiL
cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
@ -196,6 +197,7 @@ github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCS
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
@ -340,6 +342,7 @@ github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgU
github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
@ -674,7 +677,9 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -706,6 +711,7 @@ github.com/hashicorp/go-plugin v1.3.0/go.mod h1:F9eH4LrE/ZsRdbwhfjs9k9HoDUwAHnYt
github.com/hashicorp/go-plugin v1.4.1 h1:6UltRQlLN9iZO513VveELp5xyaFxVD2+1OVylE+2E+w=
github.com/hashicorp/go-plugin v1.4.1/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
@ -717,6 +723,7 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4=
github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
@ -731,10 +738,13 @@ github.com/hashicorp/hcl/v2 v2.11.1 h1:yTyWcXcm9XB0TEkyU/JCRU6rYy4K+mgLtzn2wlrJb
github.com/hashicorp/hcl/v2 v2.11.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hashicorp/terraform-json v0.13.0 h1:Li9L+lKD1FO5RVFRM1mMMIBDoUHslOniyEi5CM+FWGY=
@ -883,10 +893,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4=
github.com/kylecarbs/ice/v2 v2.1.8-0.20220221162453-b262a62902c3 h1:/SkVJxNTLozVOnU5OAQhnwt5Nb7h15c1BHad6t4h5MM=
github.com/kylecarbs/ice/v2 v2.1.8-0.20220221162453-b262a62902c3/go.mod h1:Op8jlPtjeiycsXh93Cs4jK82C9j/kh7vef6ztIOvtIQ=
github.com/kylecarbs/opencensus-go v0.23.1-0.20220220184033-4441763886a2 h1:tvKg/uBu9IqevKJECOWdHWnFxuyQZo7dBeilpx+FugY=
github.com/kylecarbs/opencensus-go v0.23.1-0.20220220184033-4441763886a2/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b h1:1Y1X6aR78kMEQE1iCjQodB3lA7VO4jB88Wf8ZrzXSsA=
github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
github.com/kylecarbs/promptui v0.8.1-0.20201231190244-d8f2159af2b2 h1:MUREBTh4kybLY1KyuBfSx+QPfTB8XiUHs6ZxUhOPTnU=
github.com/kylecarbs/promptui v0.8.1-0.20201231190244-d8f2159af2b2/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
github.com/kylecarbs/readline v0.0.0-20220211054233-0d62993714c8 h1:Y7O3Z3YeNRtw14QrtHpevU4dSjCkov0J40MtQ7Nc0n8=
@ -915,6 +923,7 @@ github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+L
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -957,9 +966,11 @@ github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKju
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@ -970,6 +981,8 @@ github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZX
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@ -1083,6 +1096,8 @@ github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5y
github.com/pion/dtls/v2 v2.1.2/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
github.com/pion/dtls/v2 v2.1.3 h1:3UF7udADqous+M2R5Uo2q/YaP4EzUoWKdfX2oscCUio=
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
github.com/pion/ice/v2 v2.2.1 h1:R3MeuJZpU1ty3diPqpD5OxaxcZ15eprAc+EtUiSoFxg=
github.com/pion/ice/v2 v2.2.1/go.mod h1:Op8jlPtjeiycsXh93Cs4jK82C9j/kh7vef6ztIOvtIQ=
github.com/pion/interceptor v0.1.7 h1:HThW0tIIKT9RRoDWGURe8rlZVOx0fJHxBHpA0ej0+bo=
github.com/pion/interceptor v0.1.7/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
@ -1130,6 +1145,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/powersj/whatsthis v1.3.0 h1:FhP+pZZr6rxBC2N/ydZOvzcFOx60Ujggy2ACYxa6Xac=
github.com/powersj/whatsthis v1.3.0/go.mod h1:8NwT2j1fdsmLLVBZ0uNPb1cvHwBHm6G3e0t/Kk7AsmI=
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@ -1209,6 +1226,7 @@ github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/snowflakedb/gosnowflake v1.6.3/go.mod h1:6hLajn6yxuJ4xUHZegMekpq9rnQbGJ7TMwXjgTmA6lg=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@ -1221,6 +1239,7 @@ github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0=
github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
@ -1233,6 +1252,7 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -1438,6 +1458,7 @@ golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -1703,6 +1724,7 @@ golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@ -1952,6 +1974,7 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=

View File

@ -27,6 +27,7 @@ func randomAPIKeyParts() (id string, secret string) {
func TestAPIKey(t *testing.T) {
t.Parallel()
successHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// Only called if the API key passes through the handler.
httpapi.Write(rw, http.StatusOK, httpapi.Response{

30
httpmw/httpmw.go Normal file
View File

@ -0,0 +1,30 @@
package httpmw
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/coder/coder/httpapi"
)
// parseUUID consumes a url parameter and parses it as a UUID.
func parseUUID(rw http.ResponseWriter, r *http.Request, param string) (uuid.UUID, bool) {
rawID := chi.URLParam(r, param)
if rawID == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("%s must be provided", param),
})
return uuid.UUID{}, false
}
parsed, err := uuid.Parse(rawID)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("%s must be a uuid", param),
})
return uuid.UUID{}, false
}
return parsed, true
}

View File

@ -40,18 +40,17 @@ func OrganizationMemberParam(r *http.Request) database.OrganizationMember {
func ExtractOrganizationParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
apiKey := APIKey(r)
organizationName := chi.URLParam(r, "organization")
if organizationName == "" {
organizationID := chi.URLParam(r, "organization")
if organizationID == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "organization name must be provided",
Message: "organization must be provided",
})
return
}
organization, err := db.GetOrganizationByName(r.Context(), organizationName)
organization, err := db.GetOrganizationByID(r.Context(), organizationID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("organization %q does not exist", organizationName),
Message: fmt.Sprintf("organization %q does not exist", organizationID),
})
return
}
@ -61,6 +60,7 @@ func ExtractOrganizationParam(db database.Store) func(http.Handler) http.Handler
})
return
}
apiKey := APIKey(r)
organizationMember, err := db.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
OrganizationID: organization.ID,
UserID: apiKey.UserID,

View File

@ -113,7 +113,7 @@ func TestOrganizationParam(t *testing.T) {
UpdatedAt: database.Now(),
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.Name)
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.ID)
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
@ -147,7 +147,7 @@ func TestOrganizationParam(t *testing.T) {
UpdatedAt: database.Now(),
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.Name)
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.ID)
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),

View File

@ -28,32 +28,25 @@ func ProjectParam(r *http.Request) database.Project {
func ExtractProjectParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
organization := OrganizationParam(r)
projectName := chi.URLParam(r, "project")
if projectName == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "project name must be provided",
})
projectID, parsed := parseUUID(rw, r, "project")
if !parsed {
return
}
project, err := db.GetProjectByOrganizationAndName(r.Context(), database.GetProjectByOrganizationAndNameParams{
OrganizationID: organization.ID,
Name: projectName,
})
project, err := db.GetProjectByID(r.Context(), projectID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("project %q does not exist", projectName),
Message: fmt.Sprintf("project %q does not exist", projectID),
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project: %s", err.Error()),
Message: fmt.Sprintf("get project: %s", err),
})
return
}
ctx := context.WithValue(r.Context(), projectParamContextKey{}, project)
chi.RouteContext(ctx).URLParams.Add("organization", project.OrganizationID)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}

View File

@ -74,7 +74,6 @@ func TestProjectParam(t *testing.T) {
require.NoError(t, err)
ctx := chi.NewRouteContext()
ctx.URLParams.Add("organization", organization.Name)
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx))
return r, organization
}
@ -83,11 +82,7 @@ func TestProjectParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractProjectParam(db),
)
rtr.Use(httpmw.ExtractProjectParam(db))
rtr.Get("/", nil)
r, _ := setupAuthentication(db)
rw := httptest.NewRecorder()
@ -102,15 +97,11 @@ func TestProjectParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractProjectParam(db),
)
rtr.Use(httpmw.ExtractProjectParam(db))
rtr.Get("/", nil)
r, _ := setupAuthentication(db)
chi.RouteContext(r.Context()).URLParams.Add("project", "nothin")
chi.RouteContext(r.Context()).URLParams.Add("project", uuid.NewString())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
@ -119,14 +110,31 @@ func TestProjectParam(t *testing.T) {
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
t.Run("BadUUID", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(httpmw.ExtractProjectParam(db))
rtr.Get("/", nil)
r, _ := setupAuthentication(db)
chi.RouteContext(r.Context()).URLParams.Add("project", "not-a-uuid")
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusBadRequest, res.StatusCode)
})
t.Run("Project", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractProjectParam(db),
httpmw.ExtractOrganizationParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.ProjectParam(r)
@ -140,7 +148,7 @@ func TestProjectParam(t *testing.T) {
Name: "moo",
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("project", project.Name)
chi.RouteContext(r.Context()).URLParams.Add("project", project.ID.String())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)

View File

@ -8,7 +8,6 @@ import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
@ -29,27 +28,14 @@ func ProjectVersionParam(r *http.Request) database.ProjectVersion {
func ExtractProjectVersionParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
project := ProjectParam(r)
projectVersionName := chi.URLParam(r, "projectversion")
if projectVersionName == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "project version name must be provided",
})
projectVersionID, parsed := parseUUID(rw, r, "projectversion")
if !parsed {
return
}
var projectVersion database.ProjectVersion
uuid, err := uuid.Parse(projectVersionName)
if err == nil {
projectVersion, err = db.GetProjectVersionByID(r.Context(), uuid)
} else {
projectVersion, err = db.GetProjectVersionByProjectIDAndName(r.Context(), database.GetProjectVersionByProjectIDAndNameParams{
ProjectID: project.ID,
Name: projectVersionName,
})
}
projectVersion, err := db.GetProjectVersionByID(r.Context(), projectVersionID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("project version %q does not exist", projectVersionName),
Message: fmt.Sprintf("project version %q does not exist", projectVersionID),
})
return
}
@ -61,6 +47,7 @@ func ExtractProjectVersionParam(db database.Store) func(http.Handler) http.Handl
}
ctx := context.WithValue(r.Context(), projectVersionParamContextKey{}, projectVersion)
chi.RouteContext(ctx).URLParams.Add("organization", projectVersion.OrganizationID)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}

View File

@ -90,12 +90,7 @@ func TestProjectVersionParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractProjectParam(db),
httpmw.ExtractProjectVersionParam(db),
)
rtr.Use(httpmw.ExtractProjectVersionParam(db))
rtr.Get("/", nil)
r, _ := setupAuthentication(db)
rw := httptest.NewRecorder()
@ -110,16 +105,11 @@ func TestProjectVersionParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractProjectParam(db),
httpmw.ExtractProjectVersionParam(db),
)
rtr.Use(httpmw.ExtractProjectVersionParam(db))
rtr.Get("/", nil)
r, _ := setupAuthentication(db)
chi.RouteContext(r.Context()).URLParams.Add("projectversion", "nothin")
chi.RouteContext(r.Context()).URLParams.Add("projectversion", uuid.NewString())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
@ -134,9 +124,8 @@ func TestProjectVersionParam(t *testing.T) {
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractProjectParam(db),
httpmw.ExtractProjectVersionParam(db),
httpmw.ExtractOrganizationParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.ProjectVersionParam(r)
@ -145,12 +134,12 @@ func TestProjectVersionParam(t *testing.T) {
r, project := setupAuthentication(db)
projectVersion, err := db.InsertProjectVersion(context.Background(), database.InsertProjectVersionParams{
ID: uuid.New(),
ProjectID: project.ID,
Name: "moo",
ID: uuid.New(),
OrganizationID: project.OrganizationID,
Name: "moo",
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("projectversion", projectVersion.Name)
chi.RouteContext(r.Context()).URLParams.Add("projectversion", projectVersion.ID.String())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)

View File

@ -1,64 +0,0 @@
package httpmw
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
)
type provisionerJobParamContextKey struct{}
// ProvisionerJobParam returns the project from the ExtractProjectParam handler.
func ProvisionerJobParam(r *http.Request) database.ProvisionerJob {
provisionerJob, ok := r.Context().Value(provisionerJobParamContextKey{}).(database.ProvisionerJob)
if !ok {
panic("developer error: provisioner job param middleware not provided")
}
return provisionerJob
}
// ExtractProvisionerJobParam grabs a provisioner job from the "provisionerjob" URL parameter.
func ExtractProvisionerJobParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
jobID := chi.URLParam(r, "provisionerjob")
if jobID == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "provisioner job must be provided",
})
return
}
jobUUID, err := uuid.Parse(jobID)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "job id must be a uuid",
})
return
}
job, err := db.GetProvisionerJobByID(r.Context(), jobUUID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: "job doesn't exist with that id",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
ctx := context.WithValue(r.Context(), provisionerJobParamContextKey{}, job)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}

View File

@ -1,109 +0,0 @@
package httpmw_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/database"
"github.com/coder/coder/database/databasefake"
"github.com/coder/coder/httpmw"
)
func TestProvisionerJobParam(t *testing.T) {
t.Parallel()
setup := func(db database.Store) (*http.Request, database.ProvisionerJob) {
r := httptest.NewRequest("GET", "/", nil)
provisionerJob, err := db.InsertProvisionerJob(context.Background(), database.InsertProvisionerJobParams{
ID: uuid.New(),
})
require.NoError(t, err)
ctx := chi.NewRouteContext()
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx))
return r, provisionerJob
}
t.Run("None", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractProvisionerJobParam(db),
)
rtr.Get("/", nil)
r, _ := setup(db)
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusBadRequest, res.StatusCode)
})
t.Run("BadUUID", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractProvisionerJobParam(db),
)
rtr.Get("/", nil)
r, _ := setup(db)
chi.RouteContext(r.Context()).URLParams.Add("provisionerjob", "nothin")
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusBadRequest, res.StatusCode)
})
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractProvisionerJobParam(db),
)
rtr.Get("/", nil)
r, _ := setup(db)
chi.RouteContext(r.Context()).URLParams.Add("provisionerjob", uuid.NewString())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
t.Run("ProvisionerJob", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractProvisionerJobParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.ProvisionerJobParam(r)
rw.WriteHeader(http.StatusOK)
})
r, job := setup(db)
chi.RouteContext(r.Context()).URLParams.Add("provisionerjob", job.ID.String())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusOK, res.StatusCode)
})
}

65
httpmw/workspaceagent.go Normal file
View File

@ -0,0 +1,65 @@
package httpmw
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/google/uuid"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
)
type workspaceAgentContextKey struct{}
// WorkspaceAgent returns the workspace agent from the ExtractAgent handler.
func WorkspaceAgent(r *http.Request) database.WorkspaceAgent {
user, ok := r.Context().Value(workspaceAgentContextKey{}).(database.WorkspaceAgent)
if !ok {
panic("developer error: agent middleware not provided")
}
return user
}
// ExtractWorkspaceAgent requires authentication using a valid agent token.
func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(AuthCookie)
if err != nil {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: fmt.Sprintf("%q cookie must be provided", AuthCookie),
})
return
}
token, err := uuid.Parse(cookie.Value)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("parse token: %s", err),
})
return
}
agent, err := db.GetWorkspaceAgentByAuthToken(r.Context(), token)
if errors.Is(err, sql.ErrNoRows) {
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "agent token is invalid",
})
return
}
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace agent: %s", err),
})
return
}
ctx := context.WithValue(r.Context(), workspaceAgentContextKey{}, agent)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}

View File

@ -0,0 +1,73 @@
package httpmw_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/database"
"github.com/coder/coder/database/databasefake"
"github.com/coder/coder/httpmw"
)
func TestWorkspaceAgent(t *testing.T) {
t.Parallel()
setup := func(db database.Store) (*http.Request, uuid.UUID) {
token := uuid.New()
r := httptest.NewRequest("GET", "/", nil)
r.AddCookie(&http.Cookie{
Name: httpmw.AuthCookie,
Value: token.String(),
})
return r, token
}
t.Run("None", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractWorkspaceAgent(db),
)
rtr.Get("/", nil)
r, _ := setup(db)
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
})
t.Run("Found", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractWorkspaceAgent(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.WorkspaceAgent(r)
rw.WriteHeader(http.StatusOK)
})
r, token := setup(db)
_, err := db.InsertWorkspaceAgent(context.Background(), database.InsertWorkspaceAgentParams{
ID: uuid.New(),
AuthToken: token,
})
require.NoError(t, err)
require.NoError(t, err)
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusOK, res.StatusCode)
})
}

View File

@ -0,0 +1,56 @@
package httpmw
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
)
type workspaceBuildParamContextKey struct{}
// WorkspaceBuildParam returns the workspace build from the ExtractWorkspaceBuildParam handler.
func WorkspaceBuildParam(r *http.Request) database.WorkspaceBuild {
workspaceBuild, ok := r.Context().Value(workspaceBuildParamContextKey{}).(database.WorkspaceBuild)
if !ok {
panic("developer error: workspace build param middleware not provided")
}
return workspaceBuild
}
// ExtractWorkspaceBuildParam grabs workspace build from the "workspacebuild" URL parameter.
func ExtractWorkspaceBuildParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
workspaceBuildID, parsed := parseUUID(rw, r, "workspacebuild")
if !parsed {
return
}
workspaceBuild, err := db.GetWorkspaceBuildByID(r.Context(), workspaceBuildID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("workspace build %q does not exist", workspaceBuildID),
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace build: %s", err.Error()),
})
return
}
ctx := context.WithValue(r.Context(), workspaceBuildParamContextKey{}, workspaceBuild)
// This injects the "workspace" parameter, because it's expected the consumer
// will want to use the Workspace middleware to ensure the caller owns the workspace.
chi.RouteContext(ctx).URLParams.Add("workspace", workspaceBuild.WorkspaceID.String())
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}

View File

@ -19,7 +19,7 @@ import (
"github.com/coder/coder/httpmw"
)
func TestWorkspaceHistoryParam(t *testing.T) {
func TestWorkspaceBuildParam(t *testing.T) {
t.Parallel()
setupAuthentication := func(db database.Store) (*http.Request, database.Workspace) {
@ -74,12 +74,7 @@ func TestWorkspaceHistoryParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractUserParam(db),
httpmw.ExtractWorkspaceParam(db),
httpmw.ExtractWorkspaceHistoryParam(db),
)
rtr.Use(httpmw.ExtractWorkspaceBuildParam(db))
rtr.Get("/", nil)
r, _ := setupAuthentication(db)
rw := httptest.NewRecorder()
@ -94,16 +89,11 @@ func TestWorkspaceHistoryParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractUserParam(db),
httpmw.ExtractWorkspaceParam(db),
httpmw.ExtractWorkspaceHistoryParam(db),
)
rtr.Use(httpmw.ExtractWorkspaceBuildParam(db))
rtr.Get("/", nil)
r, _ := setupAuthentication(db)
chi.RouteContext(r.Context()).URLParams.Add("workspacehistory", "nothin")
chi.RouteContext(r.Context()).URLParams.Add("workspacebuild", uuid.NewString())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
@ -112,60 +102,28 @@ func TestWorkspaceHistoryParam(t *testing.T) {
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
t.Run("WorkspaceHistory", func(t *testing.T) {
t.Run("WorkspaceBuild", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractUserParam(db),
httpmw.ExtractWorkspaceBuildParam(db),
httpmw.ExtractWorkspaceParam(db),
httpmw.ExtractWorkspaceHistoryParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.WorkspaceHistoryParam(r)
_ = httpmw.WorkspaceBuildParam(r)
rw.WriteHeader(http.StatusOK)
})
r, workspace := setupAuthentication(db)
workspaceHistory, err := db.InsertWorkspaceHistory(context.Background(), database.InsertWorkspaceHistoryParams{
workspaceBuild, err := db.InsertWorkspaceBuild(context.Background(), database.InsertWorkspaceBuildParams{
ID: uuid.New(),
WorkspaceID: workspace.ID,
Name: "moo",
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("workspacehistory", workspaceHistory.Name)
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusOK, res.StatusCode)
})
t.Run("WorkspaceHistoryLatest", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractUserParam(db),
httpmw.ExtractWorkspaceParam(db),
httpmw.ExtractWorkspaceHistoryParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.WorkspaceHistoryParam(r)
rw.WriteHeader(http.StatusOK)
})
r, workspace := setupAuthentication(db)
_, err := db.InsertWorkspaceHistory(context.Background(), database.InsertWorkspaceHistoryParams{
ID: uuid.New(),
WorkspaceID: workspace.ID,
Name: "moo",
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("workspacehistory", "latest")
chi.RouteContext(r.Context()).URLParams.Add("workspacebuild", workspaceBuild.ID.String())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)

View File

@ -1,72 +0,0 @@
package httpmw
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
)
type workspaceHistoryParamContextKey struct{}
// WorkspaceHistoryParam returns the workspace history from the ExtractWorkspaceHistoryParam handler.
func WorkspaceHistoryParam(r *http.Request) database.WorkspaceHistory {
workspaceHistory, ok := r.Context().Value(workspaceHistoryParamContextKey{}).(database.WorkspaceHistory)
if !ok {
panic("developer error: workspace history param middleware not provided")
}
return workspaceHistory
}
// ExtractWorkspaceHistoryParam grabs workspace history from the "workspacehistory" URL parameter.
func ExtractWorkspaceHistoryParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
workspace := WorkspaceParam(r)
workspaceHistoryName := chi.URLParam(r, "workspacehistory")
if workspaceHistoryName == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "workspace history name must be provided",
})
return
}
var workspaceHistory database.WorkspaceHistory
var err error
if workspaceHistoryName == "latest" {
workspaceHistory, err = db.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: "there is no workspace history",
})
return
}
} else {
workspaceHistory, err = db.GetWorkspaceHistoryByWorkspaceIDAndName(r.Context(), database.GetWorkspaceHistoryByWorkspaceIDAndNameParams{
WorkspaceID: workspace.ID,
Name: workspaceHistoryName,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("workspace history %q does not exist", workspaceHistoryName),
})
return
}
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace history: %s", err.Error()),
})
return
}
ctx := context.WithValue(r.Context(), workspaceHistoryParamContextKey{}, workspaceHistory)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}

View File

@ -7,8 +7,6 @@ import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
)
@ -28,18 +26,11 @@ func WorkspaceParam(r *http.Request) database.Workspace {
func ExtractWorkspaceParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
user := UserParam(r)
workspaceName := chi.URLParam(r, "workspace")
if workspaceName == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "workspace id must be provided",
})
workspaceID, parsed := parseUUID(rw, r, "workspace")
if !parsed {
return
}
workspace, err := db.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{
OwnerID: user.ID,
Name: workspaceName,
})
workspace, err := db.GetWorkspaceByID(r.Context(), workspaceID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("workspace %q does not exist", workspace),
@ -53,6 +44,14 @@ func ExtractWorkspaceParam(db database.Store) func(http.Handler) http.Handler {
return
}
apiKey := APIKey(r)
if apiKey.UserID != workspace.OwnerID {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "getting non-personal workspaces isn't supported",
})
return
}
ctx := context.WithValue(r.Context(), workspaceParamContextKey{}, workspace)
next.ServeHTTP(rw, r.WithContext(ctx))
})

View File

@ -66,11 +66,7 @@ func TestWorkspaceParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractUserParam(db),
httpmw.ExtractWorkspaceParam(db),
)
rtr.Use(httpmw.ExtractWorkspaceParam(db))
rtr.Get("/", nil)
r, _ := setup(db)
rw := httptest.NewRecorder()
@ -85,14 +81,10 @@ func TestWorkspaceParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractUserParam(db),
httpmw.ExtractWorkspaceParam(db),
)
rtr.Use(httpmw.ExtractWorkspaceParam(db))
rtr.Get("/", nil)
r, _ := setup(db)
chi.RouteContext(r.Context()).URLParams.Add("workspace", "frog")
chi.RouteContext(r.Context()).URLParams.Add("workspace", uuid.NewString())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
@ -101,13 +93,37 @@ func TestWorkspaceParam(t *testing.T) {
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
t.Run("NonPersonal", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractWorkspaceParam(db),
)
rtr.Get("/", nil)
r, _ := setup(db)
workspace, err := db.InsertWorkspace(context.Background(), database.InsertWorkspaceParams{
ID: uuid.New(),
OwnerID: "not-me",
Name: "hello",
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("workspace", workspace.ID.String())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
})
t.Run("Found", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractUserParam(db),
httpmw.ExtractWorkspaceParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
@ -121,7 +137,7 @@ func TestWorkspaceParam(t *testing.T) {
Name: "hello",
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("workspace", workspace.Name)
chi.RouteContext(r.Context()).URLParams.Add("workspace", workspace.ID.String())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)

Some files were not shown because too many files have changed in this diff Show More