feat: Add APIs for querying workspaces (#61)

* Add SQL migration

* Add query functions for workspaces

* Add create routes

* Add tests for codersdk

* Add workspace parameter route

* Add workspace query

* Move workspace function

* Add querying for workspace history

* Fix query

* Fix syntax error

* Move workspace routes

* Fix version

* Add CLI tests

* Fix syntax error

* Remove error

* Fix history error

* Add new user test

* Fix test

* Lower target to 70%

* Improve comments

* Add comment
This commit is contained in:
Kyle Carberry 2022-01-25 13:52:58 -06:00 committed by GitHub
parent 139828d594
commit 5b01f615eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 2511 additions and 81 deletions

View File

@ -16,7 +16,7 @@ coverage:
status:
project:
default:
target: 75%
target: 70%
informational: yes
ignore:

View File

@ -26,6 +26,9 @@ func New(options *Options) http.Handler {
users := &users{
Database: options.Database,
}
workspaces := &workspaces{
Database: options.Database,
}
r := chi.NewRouter()
r.Route("/api/v2", func(r chi.Router) {
@ -36,14 +39,15 @@ func New(options *Options) http.Handler {
})
r.Post("/login", users.loginWithPassword)
r.Post("/logout", users.logout)
// Used for setup.
r.Post("/user", users.createInitialUser)
r.Route("/users", func(r chi.Router) {
r.Post("/", users.createInitialUser)
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
)
r.Post("/", users.createUser)
r.Group(func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
httpmw.ExtractUserParam(options.Database),
)
r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/{user}", users.user)
r.Get("/{user}/organizations", users.userOrganizations)
})
@ -58,11 +62,33 @@ func New(options *Options) http.Handler {
r.Get("/", projects.allProjectsForOrganization)
r.Post("/", projects.createProject)
r.Route("/{project}", func(r chi.Router) {
r.Use(httpmw.ExtractProjectParameter(options.Database))
r.Use(httpmw.ExtractProjectParam(options.Database))
r.Get("/", projects.project)
r.Route("/versions", func(r chi.Router) {
r.Get("/", projects.projectVersions)
r.Post("/", projects.createProjectVersion)
r.Route("/history", func(r chi.Router) {
r.Get("/", projects.allProjectHistory)
r.Post("/", projects.createProjectHistory)
})
r.Get("/workspaces", workspaces.allWorkspacesForProject)
})
})
})
// 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("/", workspaces.listAllWorkspaces)
r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/", workspaces.listAllWorkspaces)
r.Post("/", workspaces.createWorkspaceForUser)
r.Route("/{workspace}", func(r chi.Router) {
r.Use(httpmw.ExtractWorkspaceParam(options.Database))
r.Get("/", workspaces.singleWorkspace)
r.Route("/history", func(r chi.Router) {
r.Post("/", workspaces.createWorkspaceHistory)
r.Get("/", workspaces.listAllWorkspaceHistory)
r.Get("/latest", workspaces.latestWorkspaceHistory)
})
})
})

View File

@ -24,8 +24,8 @@ import (
// abstracted for ease of change later on.
type Project database.Project
// ProjectVersion is the JSON representation of a Coder project version.
type ProjectVersion struct {
// ProjectHistory is the JSON representation of Coder project version history.
type ProjectHistory struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
CreatedAt time.Time `json:"created_at"`
@ -42,7 +42,6 @@ type CreateProjectRequest struct {
// CreateProjectVersionRequest enables callers to create a new Project Version.
type CreateProjectVersionRequest struct {
Name string `json:"name,omitempty" validate:"username"`
StorageMethod database.ProjectStorageMethod `json:"storage_method" validate:"oneof=inline-archive,required"`
StorageSource []byte `json:"storage_source" validate:"max=1048576,required"`
}
@ -51,7 +50,7 @@ type projects struct {
Database database.Store
}
// allProjects lists all projects across organizations for a user.
// Lists all projects the authenticated user has access to.
func (p *projects) allProjects(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
organizations, err := p.Database.GetOrganizationsByUserID(r.Context(), apiKey.UserID)
@ -79,7 +78,7 @@ func (p *projects) allProjects(rw http.ResponseWriter, r *http.Request) {
render.JSON(rw, r, projects)
}
// allProjectsForOrganization lists all projects for a specific organization.
// Lists all projects in an organization.
func (p *projects) allProjectsForOrganization(rw http.ResponseWriter, r *http.Request) {
organization := httpmw.OrganizationParam(r)
projects, err := p.Database.GetProjectsByOrganizationIDs(r.Context(), []string{organization.ID})
@ -96,7 +95,7 @@ func (p *projects) allProjectsForOrganization(rw http.ResponseWriter, r *http.Re
render.JSON(rw, r, projects)
}
// createProject makes a new project in an organization.
// Creates a new project in an organization.
func (p *projects) createProject(rw http.ResponseWriter, r *http.Request) {
var createProject CreateProjectRequest
if !httpapi.Read(rw, r, &createProject) {
@ -142,7 +141,7 @@ func (p *projects) createProject(rw http.ResponseWriter, r *http.Request) {
render.JSON(rw, r, project)
}
// project returns a single project parsed from the URL path.
// Returns a single project.
func (*projects) project(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)
@ -150,8 +149,8 @@ func (*projects) project(rw http.ResponseWriter, r *http.Request) {
render.JSON(rw, r, project)
}
// projectVersions lists versions for a single project.
func (p *projects) projectVersions(rw http.ResponseWriter, r *http.Request) {
// Lists history for a single project.
func (p *projects) allProjectHistory(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)
history, err := p.Database.GetProjectHistoryByProjectID(r.Context(), project.ID)
@ -164,15 +163,18 @@ func (p *projects) projectVersions(rw http.ResponseWriter, r *http.Request) {
})
return
}
versions := make([]ProjectVersion, 0)
apiHistory := make([]ProjectHistory, 0)
for _, version := range history {
versions = append(versions, convertProjectHistory(version))
apiHistory = append(apiHistory, convertProjectHistory(version))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, versions)
render.JSON(rw, r, apiHistory)
}
func (p *projects) createProjectVersion(rw http.ResponseWriter, r *http.Request) {
// 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 (p *projects) createProjectHistory(rw http.ResponseWriter, r *http.Request) {
var createProjectVersion CreateProjectVersionRequest
if !httpapi.Read(rw, r, &createProjectVersion) {
return
@ -204,6 +206,8 @@ func (p *projects) createProjectVersion(rw http.ResponseWriter, r *http.Request)
Name: namesgenerator.GetRandomName(1),
StorageMethod: createProjectVersion.StorageMethod,
StorageSource: createProjectVersion.StorageSource,
// TODO: Make this do something!
ImportJobID: uuid.New(),
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@ -218,8 +222,8 @@ func (p *projects) createProjectVersion(rw http.ResponseWriter, r *http.Request)
render.JSON(rw, r, convertProjectHistory(history))
}
func convertProjectHistory(history database.ProjectHistory) ProjectVersion {
return ProjectVersion{
func convertProjectHistory(history database.ProjectHistory) ProjectHistory {
return ProjectHistory{
ID: history.ID,
ProjectID: history.ProjectID,
CreatedAt: history.CreatedAt,

View File

@ -104,7 +104,7 @@ func TestProjects(t *testing.T) {
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
versions, err := server.Client.ProjectVersions(context.Background(), user.Organization, project.Name)
versions, err := server.Client.ProjectHistory(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.Len(t, versions, 0)
})
@ -127,13 +127,12 @@ func TestProjects(t *testing.T) {
require.NoError(t, err)
_, err = writer.Write(make([]byte, 1<<10))
require.NoError(t, err)
_, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
Name: "moo",
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: buffer.Bytes(),
})
require.NoError(t, err)
versions, err := server.Client.ProjectVersions(context.Background(), user.Organization, project.Name)
versions, err := server.Client.ProjectHistory(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.Len(t, versions, 1)
})
@ -156,8 +155,7 @@ func TestProjects(t *testing.T) {
require.NoError(t, err)
_, err = writer.Write(make([]byte, 1<<21))
require.NoError(t, err)
_, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
Name: "moo",
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: buffer.Bytes(),
})
@ -173,8 +171,7 @@ func TestProjects(t *testing.T) {
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
_, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
Name: "moo",
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: []byte{},
})

View File

@ -19,7 +19,7 @@ import (
"github.com/coder/coder/httpmw"
)
// User is the JSON representation of a Coder user.
// User represents a user in Coder.
type User struct {
ID string `json:"id" validate:"required"`
Email string `json:"email" validate:"required"`
@ -27,7 +27,9 @@ type User struct {
Username string `json:"username" validate:"required"`
}
// CreateInitialUserRequest enables callers to create a new user.
// CreateInitialUserRequest provides options to create the initial
// user for a Coder deployment. The organization provided will be
// created as well.
type CreateInitialUserRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,username"`
@ -35,6 +37,13 @@ type CreateInitialUserRequest struct {
Organization string `json:"organization" validate:"required,username"`
}
// CreateUserRequest provides options for creating a new user.
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,username"`
Password string `json:"password" validate:"required"`
}
// LoginWithPasswordRequest enables callers to authenticate with email and password.
type LoginWithPasswordRequest struct {
Email string `json:"email" validate:"required,email"`
@ -123,7 +132,58 @@ func (users *users) createInitialUser(rw http.ResponseWriter, r *http.Request) {
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, user)
render.JSON(rw, r, convertUser(user))
}
// Creates a new user.
func (users *users) createUser(rw http.ResponseWriter, r *http.Request) {
var createUser CreateUserRequest
if !httpapi.Read(rw, r, &createUser) {
return
}
_, err := users.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
Username: createUser.Username,
Email: createUser.Email,
})
if err == nil {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: "user already exists",
})
return
}
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get user: %s", err),
})
return
}
hashedPassword, err := userpassword.Hash(createUser.Password)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("hash password: %s", err.Error()),
})
return
}
user, err := users.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(),
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("create user: %s", err.Error()),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertUser(user))
}
// Returns the parameterized user requested. All validation
@ -131,12 +191,7 @@ func (users *users) createInitialUser(rw http.ResponseWriter, r *http.Request) {
func (*users) user(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
render.JSON(rw, r, User{
ID: user.ID,
Email: user.Email,
CreatedAt: user.CreatedAt,
Username: user.Username,
})
render.JSON(rw, r, convertUser(user))
}
// Returns organizations the parameterized user has access to.
@ -265,3 +320,12 @@ func generateAPIKeyIDSecret() (id string, secret string, err error) {
}
return id, secret, nil
}
func convertUser(user database.User) User {
return User{
ID: user.ID,
Email: user.Email,
CreatedAt: user.CreatedAt,
Username: user.Username,
}
}

View File

@ -76,6 +76,30 @@ func TestUsers(t *testing.T) {
require.NoError(t, err)
require.Len(t, orgs, 1)
})
t.Run("CreateUser", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
_, err := server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: "wow@ok.io",
Username: "tomato",
Password: "bananas",
})
require.NoError(t, err)
})
t.Run("CreateUserConflict", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, err := server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: "wow@ok.io",
Username: user.Username,
Password: "bananas",
})
require.Error(t, err)
})
}
func TestLogout(t *testing.T) {

365
coderd/workspaces.go Normal file
View File

@ -0,0 +1,365 @@
package coderd
import (
"database/sql"
"errors"
"fmt"
"net/http"
"time"
"github.com/go-chi/render"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
"github.com/coder/coder/httpmw"
)
// Workspace is a per-user deployment of a project. It tracks
// project versions, and can be updated.
type Workspace database.Workspace
// 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"`
CompletedAt time.Time `json:"completed_at"`
WorkspaceID uuid.UUID `json:"workspace_id"`
ProjectHistoryID uuid.UUID `json:"project_history_id"`
BeforeID uuid.UUID `json:"before_id"`
AfterID uuid.UUID `json:"after_id"`
Transition database.WorkspaceTransition `json:"transition"`
Initiator string `json:"initiator"`
}
// 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"`
}
// CreateWorkspaceHistoryRequest provides options to update the latest workspace history.
type CreateWorkspaceHistoryRequest struct {
ProjectHistoryID uuid.UUID `json:"project_history_id" validate:"required"`
Transition database.WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"`
}
type workspaces struct {
Database database.Store
}
// Returns all workspaces across all projects and organizations.
func (w *workspaces) listAllWorkspaces(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
workspaces, err := w.Database.GetWorkspacesByUserID(r.Context(), apiKey.UserID)
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)
}
// Returns all workspaces for a specific project.
func (w *workspaces) allWorkspacesForProject(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
project := httpmw.ProjectParam(r)
workspaces, err := w.Database.GetWorkspacesByProjectAndUserID(r.Context(), database.GetWorkspacesByProjectAndUserIDParams{
OwnerID: apiKey.UserID,
ProjectID: project.ID,
})
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspaces: %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)
}
// Create a new workspace for the currently authenticated user.
func (w *workspaces) createWorkspaceForUser(rw http.ResponseWriter, r *http.Request) {
var createWorkspace CreateWorkspaceRequest
if !httpapi.Read(rw, r, &createWorkspace) {
return
}
apiKey := httpmw.APIKey(r)
project, err := w.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 = w.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 := w.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 := w.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 = w.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))
}
// Returns a single singleWorkspace.
func (*workspaces) singleWorkspace(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertWorkspace(workspace))
}
// Returns all workspace history. This is not sorted. Use before/after to chronologically sort.
func (w *workspaces) listAllWorkspaceHistory(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)
histories, err := w.Database.GetWorkspaceHistoryByWorkspaceID(r.Context(), workspace.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace history: %s", err),
})
return
}
apiHistory := make([]WorkspaceHistory, 0, len(histories))
for _, history := range histories {
apiHistory = append(apiHistory, convertWorkspaceHistory(history))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiHistory)
}
// Returns the latest workspace history. This works by querying for history without "after" set.
func (w *workspaces) latestWorkspaceHistory(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)
history, err := w.Database.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: "workspace has no history",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace history: %s", err),
})
return
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertWorkspaceHistory(history))
}
// Begins transitioning a workspace to new state. This queues a provision job to asyncronously
// update the underlying infrastructure. Only one historical transition can occur at a time.
func (w *workspaces) createWorkspaceHistory(rw http.ResponseWriter, r *http.Request) {
var createBuild CreateWorkspaceHistoryRequest
if !httpapi.Read(rw, r, &createBuild) {
return
}
user := httpmw.UserParam(r)
workspace := httpmw.WorkspaceParam(r)
projectHistory, err := w.Database.GetProjectHistoryByID(r.Context(), createBuild.ProjectHistoryID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "project history not found",
Errors: []httpapi.Error{{
Field: "project_history_id",
Code: "exists",
}},
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project history: %s", err),
})
return
}
// Store prior history ID if it exists to update it after we create new!
priorHistoryID := uuid.NullUUID{}
priorHistory, err := w.Database.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
if err == nil {
if !priorHistory.CompletedAt.Valid {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: "a workspace build is already active",
})
return
}
priorHistoryID = uuid.NullUUID{
UUID: priorHistory.ID,
Valid: true,
}
}
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 = w.Database.InTx(func(db database.Store) error {
workspaceHistory, err = db.InsertWorkspaceHistory(r.Context(), database.InsertWorkspaceHistoryParams{
ID: uuid.New(),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
WorkspaceID: workspace.ID,
ProjectHistoryID: projectHistory.ID,
BeforeID: priorHistoryID,
Initiator: user.ID,
Transition: createBuild.Transition,
// This should create a provision job once that gets implemented!
ProvisionJobID: uuid.New(),
})
if err != nil {
return xerrors.Errorf("insert workspace history: %w", err)
}
if priorHistoryID.Valid {
// Update the prior history entries "after" column.
err = db.UpdateWorkspaceHistoryByID(r.Context(), database.UpdateWorkspaceHistoryByIDParams{
ID: priorHistory.ID,
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))
}
// Converts the internal workspace representation to a public external-facing model.
func convertWorkspace(workspace database.Workspace) Workspace {
return Workspace(workspace)
}
// 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,
CompletedAt: workspaceHistory.CompletedAt.Time,
WorkspaceID: workspaceHistory.WorkspaceID,
ProjectHistoryID: workspaceHistory.ProjectHistoryID,
BeforeID: workspaceHistory.BeforeID.UUID,
AfterID: workspaceHistory.AfterID.UUID,
Transition: workspaceHistory.Transition,
Initiator: workspaceHistory.Initiator,
})
}

255
coderd/workspaces_test.go Normal file
View File

@ -0,0 +1,255 @@
package coderd_test
import (
"archive/tar"
"bytes"
"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/codersdk"
"github.com/coder/coder/database"
)
func TestWorkspaces(t *testing.T) {
t.Parallel()
t.Run("ListNone", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
workspaces, err := server.Client.WorkspacesByUser(context.Background(), "")
require.NoError(t, err)
require.Len(t, workspaces, 0)
})
setupProjectAndWorkspace := func(t *testing.T, client *codersdk.Client, user coderd.CreateInitialUserRequest) (coderd.Project, coderd.Workspace) {
project, err := client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "banana",
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
workspace, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "hiii",
ProjectID: project.ID,
})
require.NoError(t, err)
return project, workspace
}
setupProjectVersion := func(t *testing.T, client *codersdk.Client, user coderd.CreateInitialUserRequest, project coderd.Project) coderd.ProjectHistory {
var buffer bytes.Buffer
writer := tar.NewWriter(&buffer)
err := writer.WriteHeader(&tar.Header{
Name: "file",
Size: 1 << 10,
})
require.NoError(t, err)
_, err = writer.Write(make([]byte, 1<<10))
require.NoError(t, err)
projectHistory, err := client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: buffer.Bytes(),
})
require.NoError(t, err)
return projectHistory
}
t.Run("List", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, _ = setupProjectAndWorkspace(t, server.Client, user)
workspaces, err := server.Client.WorkspacesByUser(context.Background(), "")
require.NoError(t, err)
require.Len(t, workspaces, 1)
})
t.Run("ListNoneForProject", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "banana",
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
workspaces, err := server.Client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.Len(t, workspaces, 0)
})
t.Run("ListForProject", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, _ := setupProjectAndWorkspace(t, server.Client, user)
workspaces, err := server.Client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.Len(t, workspaces, 1)
})
t.Run("CreateInvalidInput", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "banana",
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
_, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
ProjectID: project.ID,
Name: "$$$",
})
require.Error(t, err)
})
t.Run("CreateInvalidProject", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
_, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
ProjectID: uuid.New(),
Name: "moo",
})
require.Error(t, err)
})
t.Run("CreateNotInProjectOrganization", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
initial := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), initial.Organization, coderd.CreateProjectRequest{
Name: "banana",
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
_, err = server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: "hello@ok.io",
Username: "example",
Password: "wowowow",
})
require.NoError(t, err)
token, err := server.Client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: "hello@ok.io",
Password: "wowowow",
})
require.NoError(t, err)
err = server.Client.SetSessionToken(token.SessionToken)
require.NoError(t, err)
_, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
ProjectID: project.ID,
Name: "moo",
})
require.Error(t, err)
})
t.Run("CreateAlreadyExists", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, workspace := setupProjectAndWorkspace(t, server.Client, user)
_, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: workspace.Name,
ProjectID: project.ID,
})
require.Error(t, err)
})
t.Run("Single", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, workspace := setupProjectAndWorkspace(t, server.Client, user)
_, err := server.Client.Workspace(context.Background(), "", workspace.Name)
require.NoError(t, err)
})
t.Run("AllHistory", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, workspace := setupProjectAndWorkspace(t, server.Client, user)
history, err := server.Client.WorkspaceHistory(context.Background(), "", workspace.Name)
require.NoError(t, err)
require.Len(t, history, 0)
projectVersion := setupProjectVersion(t, server.Client, user, project)
_, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectHistoryID: projectVersion.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
history, err = server.Client.WorkspaceHistory(context.Background(), "", workspace.Name)
require.NoError(t, err)
require.Len(t, history, 1)
})
t.Run("LatestHistory", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, workspace := setupProjectAndWorkspace(t, server.Client, user)
_, err := server.Client.LatestWorkspaceHistory(context.Background(), "", workspace.Name)
require.Error(t, err)
projectVersion := setupProjectVersion(t, server.Client, user, project)
_, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectHistoryID: projectVersion.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
_, err = server.Client.LatestWorkspaceHistory(context.Background(), "", workspace.Name)
require.NoError(t, err)
})
t.Run("CreateHistory", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, workspace := setupProjectAndWorkspace(t, server.Client, user)
projectHistory := setupProjectVersion(t, server.Client, user, project)
_, err := server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectHistoryID: projectHistory.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
})
t.Run("CreateHistoryAlreadyInProgress", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, workspace := setupProjectAndWorkspace(t, server.Client, user)
projectHistory := setupProjectVersion(t, server.Client, user, project)
_, err := server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectHistoryID: projectHistory.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
_, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectHistoryID: projectHistory.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.Error(t, err)
})
t.Run("CreateHistoryInvalidProjectVersion", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, workspace := setupProjectAndWorkspace(t, server.Client, user)
_, err := server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectHistoryID: uuid.New(),
Transition: database.WorkspaceTransitionCreate,
})
require.Error(t, err)
})
}

View File

@ -57,9 +57,9 @@ func (c *Client) CreateProject(ctx context.Context, organization string, request
return project, json.NewDecoder(res.Body).Decode(&project)
}
// ProjectVersions lists history for 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)
// ProjectHistory lists history for a project.
func (c *Client) ProjectHistory(ctx context.Context, organization, project string) ([]coderd.ProjectHistory, error) {
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/%s/history", organization, project), nil)
if err != nil {
return nil, err
}
@ -67,20 +67,20 @@ func (c *Client) ProjectVersions(ctx context.Context, organization, project stri
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var projectVersions []coderd.ProjectVersion
var projectVersions []coderd.ProjectHistory
return projectVersions, json.NewDecoder(res.Body).Decode(&projectVersions)
}
// 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)
// CreateProjectHistory inserts a new version for the project.
func (c *Client) CreateProjectHistory(ctx context.Context, organization, project string, request coderd.CreateProjectVersionRequest) (coderd.ProjectHistory, error) {
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/projects/%s/%s/history", organization, project), request)
if err != nil {
return coderd.ProjectVersion{}, err
return coderd.ProjectHistory{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return coderd.ProjectVersion{}, readBodyAsError(res)
return coderd.ProjectHistory{}, readBodyAsError(res)
}
var projectVersion coderd.ProjectVersion
var projectVersion coderd.ProjectHistory
return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion)
}

View File

@ -74,7 +74,7 @@ func TestProjects(t *testing.T) {
t.Run("UnauthenticatedVersions", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.ProjectVersions(context.Background(), "org", "project")
_, err := server.Client.ProjectHistory(context.Background(), "org", "project")
require.Error(t, err)
})
@ -87,15 +87,14 @@ func TestProjects(t *testing.T) {
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
_, err = server.Client.ProjectVersions(context.Background(), user.Organization, project.Name)
_, err = server.Client.ProjectHistory(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
})
t.Run("CreateVersionUnauthenticated", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.CreateProjectVersion(context.Background(), "org", "project", coderd.CreateProjectVersionRequest{
Name: "hello",
_, err := server.Client.CreateProjectHistory(context.Background(), "org", "project", coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: []byte{},
})
@ -120,8 +119,7 @@ func TestProjects(t *testing.T) {
require.NoError(t, err)
_, err = writer.Write(make([]byte, 1<<10))
require.NoError(t, err)
_, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
Name: "hello",
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: buffer.Bytes(),
})

View File

@ -13,6 +13,20 @@ import (
// 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)
if err != nil {
return coderd.User{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return coderd.User{}, readBodyAsError(res)
}
var user coderd.User
return user, json.NewDecoder(res.Body).Decode(&user)
}
// CreateUser creates a new user.
func (c *Client) CreateUser(ctx context.Context, req coderd.CreateUserRequest) (coderd.User, error) {
res, err := c.request(ctx, http.MethodPost, "/api/v2/users", req)
if err != nil {
return coderd.User{}, err

View File

@ -55,4 +55,16 @@ func TestUsers(t *testing.T) {
err := server.Client.Logout(context.Background())
require.NoError(t, err)
})
t.Run("CreateMultiple", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
_, err := server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: "wow@ok.io",
Username: "example",
Password: "tomato",
})
require.NoError(t, err)
})
}

129
codersdk/workspaces.go Normal file
View File

@ -0,0 +1,129 @@
package codersdk
import (
"context"
"encoding/json"
"fmt"
"net/http"
"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) WorkspacesByUser(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)
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)
}
// WorkspaceHistory returns historical data for workspace builds.
func (c *Client) WorkspaceHistory(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/history", owner, workspace), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var workspaceHistory []coderd.WorkspaceHistory
return workspaceHistory, json.NewDecoder(res.Body).Decode(&workspaceHistory)
}
// LatestWorkspaceHistory returns the newest build for a workspace.
func (c *Client) LatestWorkspaceHistory(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/history/latest", owner, workspace), nil)
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
}
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)
}
// 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/history", owner, workspace), request)
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)
}

169
codersdk/workspaces_test.go Normal file
View File

@ -0,0 +1,169 @@
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 TestWorkspaces(t *testing.T) {
t.Parallel()
t.Run("ListError", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.WorkspacesByUser(context.Background(), "")
require.Error(t, err)
})
t.Run("ListNoOwner", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.WorkspacesByUser(context.Background(), "")
require.Error(t, err)
})
t.Run("ListByUser", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "tomato",
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
_, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "wooow",
ProjectID: project.ID,
})
require.NoError(t, err)
_, err = server.Client.WorkspacesByUser(context.Background(), "me")
require.NoError(t, err)
})
t.Run("ListByProject", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "tomato",
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
_, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "wooow",
ProjectID: project.ID,
})
require.NoError(t, err)
_, err = server.Client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
})
t.Run("ListByProjectError", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.WorkspacesByProject(context.Background(), "", "")
require.Error(t, err)
})
t.Run("CreateError", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.CreateWorkspace(context.Background(), "no", coderd.CreateWorkspaceRequest{})
require.Error(t, err)
})
t.Run("Single", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "tomato",
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "wooow",
ProjectID: project.ID,
})
require.NoError(t, err)
_, err = server.Client.Workspace(context.Background(), "", workspace.Name)
require.NoError(t, err)
})
t.Run("SingleError", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.Workspace(context.Background(), "", "blob")
require.Error(t, err)
})
t.Run("History", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "tomato",
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "wooow",
ProjectID: project.ID,
})
require.NoError(t, err)
_, err = server.Client.WorkspaceHistory(context.Background(), "", workspace.Name)
require.NoError(t, err)
})
t.Run("HistoryError", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.WorkspaceHistory(context.Background(), "", "blob")
require.Error(t, err)
})
t.Run("LatestHistory", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "tomato",
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "wooow",
ProjectID: project.ID,
})
require.NoError(t, err)
_, err = server.Client.LatestWorkspaceHistory(context.Background(), "", workspace.Name)
require.Error(t, err)
})
t.Run("CreateHistory", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "tomato",
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "wooow",
ProjectID: project.ID,
})
require.NoError(t, err)
_, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectHistoryID: uuid.New(),
Transition: database.WorkspaceTransitionCreate,
})
require.Error(t, err)
})
}

View File

@ -18,9 +18,13 @@ func New() database.Store {
organizationMembers: make([]database.OrganizationMember, 0),
users: make([]database.User, 0),
project: make([]database.Project, 0),
projectHistory: make([]database.ProjectHistory, 0),
projectParameter: make([]database.ProjectParameter, 0),
project: make([]database.Project, 0),
projectHistory: make([]database.ProjectHistory, 0),
projectParameter: make([]database.ProjectParameter, 0),
workspace: make([]database.Workspace, 0),
workspaceResource: make([]database.WorkspaceResource, 0),
workspaceHistory: make([]database.WorkspaceHistory, 0),
workspaceAgent: make([]database.WorkspaceAgent, 0),
}
}
@ -33,9 +37,13 @@ type fakeQuerier struct {
users []database.User
// New tables
project []database.Project
projectHistory []database.ProjectHistory
projectParameter []database.ProjectParameter
project []database.Project
projectHistory []database.ProjectHistory
projectParameter []database.ProjectParameter
workspace []database.Workspace
workspaceResource []database.WorkspaceResource
workspaceHistory []database.WorkspaceHistory
workspaceAgent []database.WorkspaceAgent
}
// InTx doesn't rollback data properly for in-memory yet.
@ -74,6 +82,103 @@ func (q *fakeQuerier) GetUserCount(_ context.Context) (int64, error) {
return int64(len(q.users)), nil
}
func (q *fakeQuerier) GetWorkspaceAgentsByResourceIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceAgent, error) {
agents := make([]database.WorkspaceAgent, 0)
for _, workspaceAgent := range q.workspaceAgent {
for _, id := range ids {
if workspaceAgent.WorkspaceResourceID.String() == id.String() {
agents = append(agents, workspaceAgent)
}
}
}
if len(agents) == 0 {
return nil, sql.ErrNoRows
}
return agents, nil
}
func (q *fakeQuerier) GetWorkspaceByUserIDAndName(_ context.Context, arg database.GetWorkspaceByUserIDAndNameParams) (database.Workspace, error) {
for _, workspace := range q.workspace {
if workspace.OwnerID != arg.OwnerID {
continue
}
if !strings.EqualFold(workspace.Name, arg.Name) {
continue
}
return workspace, nil
}
return database.Workspace{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetWorkspaceResourcesByHistoryID(_ context.Context, workspaceHistoryID uuid.UUID) ([]database.WorkspaceResource, error) {
resources := make([]database.WorkspaceResource, 0)
for _, workspaceResource := range q.workspaceResource {
if workspaceResource.WorkspaceHistoryID.String() == workspaceHistoryID.String() {
resources = append(resources, workspaceResource)
}
}
if len(resources) == 0 {
return nil, sql.ErrNoRows
}
return resources, nil
}
func (q *fakeQuerier) GetWorkspaceHistoryByWorkspaceIDWithoutAfter(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceHistory, error) {
for _, workspaceHistory := range q.workspaceHistory {
if workspaceHistory.WorkspaceID.String() != workspaceID.String() {
continue
}
if !workspaceHistory.AfterID.Valid {
return workspaceHistory, nil
}
}
return database.WorkspaceHistory{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetWorkspaceHistoryByWorkspaceID(_ context.Context, workspaceID uuid.UUID) ([]database.WorkspaceHistory, error) {
history := make([]database.WorkspaceHistory, 0)
for _, workspaceHistory := range q.workspaceHistory {
if workspaceHistory.WorkspaceID.String() == workspaceID.String() {
history = append(history, workspaceHistory)
}
}
if len(history) == 0 {
return nil, sql.ErrNoRows
}
return history, nil
}
func (q *fakeQuerier) GetWorkspacesByProjectAndUserID(_ context.Context, arg database.GetWorkspacesByProjectAndUserIDParams) ([]database.Workspace, error) {
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
}
func (q *fakeQuerier) GetWorkspacesByUserID(_ context.Context, ownerID string) ([]database.Workspace, error) {
workspaces := make([]database.Workspace, 0)
for _, workspace := range q.workspace {
if workspace.OwnerID != ownerID {
continue
}
workspaces = append(workspaces, workspace)
}
if len(workspaces) == 0 {
return nil, sql.ErrNoRows
}
return workspaces, nil
}
func (q *fakeQuerier) GetOrganizationByName(_ context.Context, name string) (database.Organization, error) {
for _, organization := range q.organizations {
if organization.Name == name {
@ -102,6 +207,15 @@ func (q *fakeQuerier) GetOrganizationsByUserID(_ context.Context, userID string)
return organizations, nil
}
func (q *fakeQuerier) GetProjectByID(_ context.Context, id uuid.UUID) (database.Project, error) {
for _, project := range q.project {
if project.ID.String() == id.String() {
return project, nil
}
}
return database.Project{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetProjectByOrganizationAndName(_ context.Context, arg database.GetProjectByOrganizationAndNameParams) (database.Project, error) {
for _, project := range q.project {
if project.OrganizationID != arg.OrganizationID {
@ -129,6 +243,16 @@ func (q *fakeQuerier) GetProjectHistoryByProjectID(_ context.Context, projectID
return history, nil
}
func (q *fakeQuerier) GetProjectHistoryByID(_ context.Context, projectHistoryID uuid.UUID) (database.ProjectHistory, error) {
for _, projectHistory := range q.projectHistory {
if projectHistory.ID.String() != projectHistoryID.String() {
continue
}
return projectHistory, nil
}
return database.ProjectHistory{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetProjectsByOrganizationIDs(_ context.Context, ids []string) ([]database.Project, error) {
projects := make([]database.Project, 0)
for _, project := range q.project {
@ -273,6 +397,63 @@ func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
return user, nil
}
func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWorkspaceParams) (database.Workspace, error) {
//nolint:gosimple
workspace := database.Workspace{
ID: arg.ID,
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
OwnerID: arg.OwnerID,
ProjectID: arg.ProjectID,
Name: arg.Name,
}
q.workspace = append(q.workspace, workspace)
return workspace, nil
}
func (q *fakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.InsertWorkspaceAgentParams) (database.WorkspaceAgent, error) {
//nolint:gosimple
workspaceAgent := database.WorkspaceAgent{
ID: arg.ID,
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
WorkspaceResourceID: arg.WorkspaceResourceID,
InstanceMetadata: arg.InstanceMetadata,
ResourceMetadata: arg.ResourceMetadata,
}
q.workspaceAgent = append(q.workspaceAgent, workspaceAgent)
return workspaceAgent, nil
}
func (q *fakeQuerier) InsertWorkspaceHistory(_ context.Context, arg database.InsertWorkspaceHistoryParams) (database.WorkspaceHistory, error) {
workspaceHistory := database.WorkspaceHistory{
ID: arg.ID,
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
WorkspaceID: arg.WorkspaceID,
ProjectHistoryID: arg.ProjectHistoryID,
BeforeID: arg.BeforeID,
Transition: arg.Transition,
Initiator: arg.Initiator,
ProvisionJobID: arg.ProvisionJobID,
}
q.workspaceHistory = append(q.workspaceHistory, workspaceHistory)
return workspaceHistory, nil
}
func (q *fakeQuerier) InsertWorkspaceResource(_ context.Context, arg database.InsertWorkspaceResourceParams) (database.WorkspaceResource, error) {
workspaceResource := database.WorkspaceResource{
ID: arg.ID,
CreatedAt: arg.CreatedAt,
WorkspaceHistoryID: arg.WorkspaceHistoryID,
Type: arg.Type,
Name: arg.Name,
WorkspaceAgentToken: arg.WorkspaceAgentToken,
}
q.workspaceResource = append(q.workspaceResource, workspaceResource)
return workspaceResource, nil
}
func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPIKeyByIDParams) error {
for index, apiKey := range q.apiKeys {
if apiKey.ID != arg.ID {
@ -288,3 +469,16 @@ func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPI
}
return sql.ErrNoRows
}
func (q *fakeQuerier) UpdateWorkspaceHistoryByID(_ context.Context, arg database.UpdateWorkspaceHistoryByIDParams) error {
for index, workspaceHistory := range q.workspaceHistory {
if workspaceHistory.ID.String() != arg.ID.String() {
continue
}
workspaceHistory.UpdatedAt = arg.UpdatedAt
workspaceHistory.AfterID = arg.AfterID
q.workspaceHistory[index] = workspaceHistory
return nil
}
return sql.ErrNoRows
}

View File

@ -1,5 +1,14 @@
-- Code generated by 'make database/generate'. DO NOT EDIT.
CREATE TYPE log_level AS ENUM (
'trace',
'debug',
'info',
'warn',
'error',
'fatal'
);
CREATE TYPE login_type AS ENUM (
'built-in',
'saml',
@ -25,6 +34,13 @@ CREATE TYPE userstatus AS ENUM (
'decommissioned'
);
CREATE TYPE workspace_transition AS ENUM (
'create',
'start',
'stop',
'delete'
);
CREATE TABLE api_keys (
id text NOT NULL,
hashed_secret bytea NOT NULL,
@ -132,6 +148,58 @@ CREATE TABLE users (
shell text DEFAULT ''::text NOT NULL
);
CREATE TABLE workspace (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
owner_id text NOT NULL,
project_id uuid NOT NULL,
name character varying(64) NOT NULL
);
CREATE TABLE workspace_agent (
id uuid NOT NULL,
workspace_resource_id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
instance_metadata jsonb NOT NULL,
resource_metadata jsonb NOT NULL
);
CREATE TABLE workspace_history (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
completed_at timestamp with time zone,
workspace_id uuid NOT NULL,
project_history_id uuid NOT NULL,
before_id uuid,
after_id uuid,
transition workspace_transition NOT NULL,
initiator character varying(255) NOT NULL,
provisioner_state bytea,
provision_job_id uuid NOT NULL
);
CREATE TABLE workspace_log (
workspace_id uuid NOT NULL,
workspace_history_id uuid NOT NULL,
created timestamp with time zone NOT NULL,
logged_by character varying(255),
level log_level NOT NULL,
log jsonb NOT NULL
);
CREATE TABLE workspace_resource (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
workspace_history_id uuid NOT NULL,
type character varying(256) NOT NULL,
name character varying(64) NOT NULL,
workspace_agent_token character varying(128) NOT NULL,
workspace_agent_id uuid
);
ALTER TABLE ONLY project_history
ADD CONSTRAINT project_history_id_key UNIQUE (id);
@ -150,9 +218,50 @@ ALTER TABLE ONLY project_parameter
ALTER TABLE ONLY project_parameter
ADD CONSTRAINT project_parameter_project_history_id_name_key UNIQUE (project_history_id, name);
ALTER TABLE ONLY workspace_agent
ADD CONSTRAINT workspace_agent_id_key UNIQUE (id);
ALTER TABLE ONLY workspace_history
ADD CONSTRAINT workspace_history_id_key UNIQUE (id);
ALTER TABLE ONLY workspace
ADD CONSTRAINT workspace_id_key UNIQUE (id);
ALTER TABLE ONLY workspace_resource
ADD CONSTRAINT workspace_resource_id_key UNIQUE (id);
ALTER TABLE ONLY workspace_resource
ADD CONSTRAINT workspace_resource_workspace_agent_token_key UNIQUE (workspace_agent_token);
ALTER TABLE ONLY workspace_resource
ADD CONSTRAINT workspace_resource_workspace_history_id_name_key UNIQUE (workspace_history_id, name);
CREATE INDEX workspace_log_index ON workspace_log USING btree (workspace_id, workspace_history_id);
ALTER TABLE ONLY project_history
ADD CONSTRAINT project_history_project_id_fkey FOREIGN KEY (project_id) REFERENCES project(id);
ALTER TABLE ONLY project_parameter
ADD CONSTRAINT project_parameter_project_history_id_fkey FOREIGN KEY (project_history_id) REFERENCES project_history(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_agent
ADD CONSTRAINT workspace_agent_workspace_resource_id_fkey FOREIGN KEY (workspace_resource_id) REFERENCES workspace_resource(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_history
ADD CONSTRAINT workspace_history_project_history_id_fkey FOREIGN KEY (project_history_id) REFERENCES project_history(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_log
ADD CONSTRAINT workspace_log_workspace_history_id_fkey FOREIGN KEY (workspace_history_id) REFERENCES workspace_history(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_log
ADD CONSTRAINT workspace_log_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_workspace_history_id_fkey FOREIGN KEY (workspace_history_id) REFERENCES workspace_history(id) ON DELETE CASCADE;

View File

@ -0,0 +1,90 @@
CREATE TABLE workspace (
id uuid NOT NULL UNIQUE,
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL,
owner_id text NOT NULL,
project_id uuid NOT NULL REFERENCES project (id),
name varchar(64) NOT NULL
);
CREATE TYPE workspace_transition AS ENUM (
'create',
'start',
'stop',
'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,
completed_at timestamptz,
workspace_id uuid NOT NULL REFERENCES workspace (id) ON DELETE CASCADE,
project_history_id uuid NOT NULL REFERENCES project_history (id) ON DELETE CASCADE,
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
);
-- Cloud resources produced by a provision job.
CREATE TABLE workspace_resource (
id uuid NOT NULL UNIQUE,
created_at timestamptz NOT NULL,
workspace_history_id uuid NOT NULL REFERENCES workspace_history (id) ON DELETE CASCADE,
-- Resource type produced by a provisioner.
-- eg. "google_compute_instance"
type varchar(256) NOT NULL,
-- Name of the resource.
-- eg. "kyle-dev-instance"
name varchar(64) NOT NULL,
-- Token for an agent to connect.
workspace_agent_token varchar(128) NOT NULL UNIQUE,
-- If an agent has been conencted for this resource,
-- the agent table is not null.
workspace_agent_id uuid,
UNIQUE(workspace_history_id, name)
);
CREATE TABLE workspace_agent (
id uuid NOT NULL UNIQUE,
workspace_resource_id uuid NOT NULL REFERENCES workspace_resource (id) ON DELETE CASCADE,
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL,
-- Identifies instance architecture, cloud, etc.
instance_metadata jsonb NOT NULL,
-- Identifies resources.
resource_metadata jsonb NOT NULL
);
CREATE TYPE log_level AS ENUM (
'trace',
'debug',
'info',
'warn',
'error',
'fatal'
);
CREATE TABLE workspace_log (
workspace_id uuid NOT NULL REFERENCES workspace (id) ON DELETE CASCADE,
-- workspace_history_id can be NULL because some events are not going to be part of a
-- deliberate transition, e.g. an infrastructure failure that kills the workspace
workspace_history_id uuid NOT NULL REFERENCES workspace_history (id) ON DELETE CASCADE,
created timestamptz NOT NULL,
-- not sure this is necessary, also not sure it's necessary separate from the log column
logged_by varchar(255),
level log_level NOT NULL,
log jsonb NOT NULL
);
CREATE INDEX workspace_log_index ON workspace_log (
workspace_id,
workspace_history_id
);

View File

@ -11,6 +11,29 @@ import (
"github.com/google/uuid"
)
type LogLevel string
const (
LogLevelTrace LogLevel = "trace"
LogLevelDebug LogLevel = "debug"
LogLevelInfo LogLevel = "info"
LogLevelWarn LogLevel = "warn"
LogLevelError LogLevel = "error"
LogLevelFatal LogLevel = "fatal"
)
func (e *LogLevel) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = LogLevel(s)
case string:
*e = LogLevel(s)
default:
return fmt.Errorf("unsupported scan type for LogLevel: %T", src)
}
return nil
}
type LoginType string
const (
@ -106,6 +129,27 @@ func (e *UserStatus) Scan(src interface{}) error {
return nil
}
type WorkspaceTransition string
const (
WorkspaceTransitionCreate WorkspaceTransition = "create"
WorkspaceTransitionStart WorkspaceTransition = "start"
WorkspaceTransitionStop WorkspaceTransition = "stop"
WorkspaceTransitionDelete WorkspaceTransition = "delete"
)
func (e *WorkspaceTransition) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = WorkspaceTransition(s)
case string:
*e = WorkspaceTransition(s)
default:
return fmt.Errorf("unsupported scan type for WorkspaceTransition: %T", src)
}
return nil
}
type APIKey struct {
ID string `db:"id" json:"id"`
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
@ -212,3 +256,55 @@ type User struct {
Decomissioned bool `db:"_decomissioned" json:"_decomissioned"`
Shell string `db:"shell" json:"shell"`
}
type Workspace 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"`
OwnerID string `db:"owner_id" json:"owner_id"`
ProjectID uuid.UUID `db:"project_id" json:"project_id"`
Name string `db:"name" json:"name"`
}
type WorkspaceAgent struct {
ID uuid.UUID `db:"id" json:"id"`
WorkspaceResourceID uuid.UUID `db:"workspace_resource_id" json:"workspace_resource_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
InstanceMetadata json.RawMessage `db:"instance_metadata" json:"instance_metadata"`
ResourceMetadata json.RawMessage `db:"resource_metadata" json:"resource_metadata"`
}
type WorkspaceHistory 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"`
CompletedAt sql.NullTime `db:"completed_at" json:"completed_at"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
ProjectHistoryID uuid.UUID `db:"project_history_id" json:"project_history_id"`
BeforeID uuid.NullUUID `db:"before_id" json:"before_id"`
AfterID uuid.NullUUID `db:"after_id" json:"after_id"`
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"`
}
type WorkspaceLog struct {
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
WorkspaceHistoryID uuid.UUID `db:"workspace_history_id" json:"workspace_history_id"`
Created time.Time `db:"created" json:"created"`
LoggedBy sql.NullString `db:"logged_by" json:"logged_by"`
Level LogLevel `db:"level" json:"level"`
Log json.RawMessage `db:"log" json:"log"`
}
type WorkspaceResource struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
WorkspaceHistoryID uuid.UUID `db:"workspace_history_id" json:"workspace_history_id"`
Type string `db:"type" json:"type"`
Name string `db:"name" json:"name"`
WorkspaceAgentToken string `db:"workspace_agent_token" json:"workspace_agent_token"`
WorkspaceAgentID uuid.NullUUID `db:"workspace_agent_id" json:"workspace_agent_id"`
}

View File

@ -13,12 +13,21 @@ type querier interface {
GetOrganizationByName(ctx context.Context, name string) (Organization, error)
GetOrganizationMemberByUserID(ctx context.Context, arg GetOrganizationMemberByUserIDParams) (OrganizationMember, error)
GetOrganizationsByUserID(ctx context.Context, userID string) ([]Organization, error)
GetProjectByID(ctx context.Context, id uuid.UUID) (Project, error)
GetProjectByOrganizationAndName(ctx context.Context, arg GetProjectByOrganizationAndNameParams) (Project, error)
GetProjectHistoryByID(ctx context.Context, id uuid.UUID) (ProjectHistory, error)
GetProjectHistoryByProjectID(ctx context.Context, projectID uuid.UUID) ([]ProjectHistory, error)
GetProjectsByOrganizationIDs(ctx context.Context, ids []string) ([]Project, error)
GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error)
GetUserByID(ctx context.Context, id string) (User, error)
GetUserCount(ctx context.Context) (int64, error)
GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error)
GetWorkspaceByUserIDAndName(ctx context.Context, arg GetWorkspaceByUserIDAndNameParams) (Workspace, error)
GetWorkspaceHistoryByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceHistory, error)
GetWorkspaceHistoryByWorkspaceIDWithoutAfter(ctx context.Context, workspaceID uuid.UUID) (WorkspaceHistory, error)
GetWorkspaceResourcesByHistoryID(ctx context.Context, workspaceHistoryID uuid.UUID) ([]WorkspaceResource, error)
GetWorkspacesByProjectAndUserID(ctx context.Context, arg GetWorkspacesByProjectAndUserIDParams) ([]Workspace, error)
GetWorkspacesByUserID(ctx context.Context, ownerID string) ([]Workspace, error)
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error)
InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error)
@ -26,7 +35,12 @@ type querier interface {
InsertProjectHistory(ctx context.Context, arg InsertProjectHistoryParams) (ProjectHistory, error)
InsertProjectParameter(ctx context.Context, arg InsertProjectParameterParams) (ProjectParameter, error)
InsertUser(ctx context.Context, arg InsertUserParams) (User, error)
InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error)
InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error)
InsertWorkspaceHistory(ctx context.Context, arg InsertWorkspaceHistoryParams) (WorkspaceHistory, error)
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error)
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
UpdateWorkspaceHistoryByID(ctx context.Context, arg UpdateWorkspaceHistoryByIDParams) error
}
var _ querier = (*sqlQuerier)(nil)

View File

@ -30,8 +30,8 @@ SELECT
FROM
users
WHERE
username = $1
OR email = $2
LOWER(username) = LOWER(@username)
OR email = @email
LIMIT
1;
@ -47,7 +47,7 @@ SELECT
FROM
organizations
WHERE
name = $1
LOWER(name) = LOWER(@name)
LIMIT
1;
@ -77,14 +77,24 @@ WHERE
LIMIT
1;
-- name: GetProjectByID :one
SELECT
*
FROM
project
WHERE
id = $1
LIMIT
1;
-- name: GetProjectByOrganizationAndName :one
SELECT
*
FROM
project
WHERE
organization_id = $1
AND name = $2
organization_id = @organization_id
AND LOWER(name) = LOWER(@name)
LIMIT
1;
@ -104,6 +114,75 @@ FROM
WHERE
project_id = $1;
-- name: GetProjectHistoryByID :one
SELECT
*
FROM
project_history
WHERE
id = $1;
-- name: GetWorkspacesByUserID :many
SELECT
*
FROM
workspace
WHERE
owner_id = $1;
-- name: GetWorkspaceByUserIDAndName :one
SELECT
*
FROM
workspace
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: GetWorkspaceHistoryByWorkspaceID :many
SELECT
*
FROM
workspace_history
WHERE
workspace_id = $1;
-- name: GetWorkspaceHistoryByWorkspaceIDWithoutAfter :one
SELECT
*
FROM
workspace_history
WHERE
workspace_id = $1
AND after_id IS NULL
LIMIT
1;
-- name: GetWorkspaceResourcesByHistoryID :many
SELECT
*
FROM
workspace_resource
WHERE
workspace_history_id = $1;
-- name: GetWorkspaceAgentsByResourceIDs :many
SELECT
*
FROM
workspace_agent
WHERE
workspace_resource_id = ANY(@ids :: uuid [ ]);
-- name: InsertAPIKey :one
INSERT INTO
api_keys (
@ -243,6 +322,61 @@ INSERT INTO
VALUES
($1, $2, $3, $4, false, $5, $6, $7, $8) RETURNING *;
-- name: InsertWorkspace :one
INSERT INTO
workspace (
id,
created_at,
updated_at,
owner_id,
project_id,
name
)
VALUES
($1, $2, $3, $4, $5, $6) RETURNING *;
-- name: InsertWorkspaceAgent :one
INSERT INTO
workspace_agent (
id,
workspace_resource_id,
created_at,
updated_at,
instance_metadata,
resource_metadata
)
VALUES
($1, $2, $3, $4, $5, $6) RETURNING *;
-- name: InsertWorkspaceHistory :one
INSERT INTO
workspace_history (
id,
created_at,
updated_at,
workspace_id,
project_history_id,
before_id,
transition,
initiator,
provision_job_id
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *;
-- name: InsertWorkspaceResource :one
INSERT INTO
workspace_resource (
id,
created_at,
workspace_history_id,
type,
name,
workspace_agent_token
)
VALUES
($1, $2, $3, $4, $5, $6) RETURNING *;
-- name: UpdateAPIKeyByID :exec
UPDATE
api_keys
@ -254,3 +388,12 @@ SET
oidc_expiry = $6
WHERE
id = $1;
-- name: UpdateWorkspaceHistoryByID :exec
UPDATE
workspace_history
SET
updated_at = $2,
after_id = $3
WHERE
id = $1;

View File

@ -6,6 +6,7 @@ package database
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/google/uuid"
@ -52,7 +53,7 @@ SELECT
FROM
organizations
WHERE
name = $1
LOWER(name) = LOWER($1)
LIMIT
1
`
@ -155,6 +156,32 @@ func (q *sqlQuerier) GetOrganizationsByUserID(ctx context.Context, userID string
return items, nil
}
const getProjectByID = `-- name: GetProjectByID :one
SELECT
id, created_at, updated_at, organization_id, name, provisioner, active_version_id
FROM
project
WHERE
id = $1
LIMIT
1
`
func (q *sqlQuerier) GetProjectByID(ctx context.Context, id uuid.UUID) (Project, error) {
row := q.db.QueryRowContext(ctx, getProjectByID, id)
var i Project
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OrganizationID,
&i.Name,
&i.Provisioner,
&i.ActiveVersionID,
)
return i, err
}
const getProjectByOrganizationAndName = `-- name: GetProjectByOrganizationAndName :one
SELECT
id, created_at, updated_at, organization_id, name, provisioner, active_version_id
@ -162,7 +189,7 @@ FROM
project
WHERE
organization_id = $1
AND name = $2
AND LOWER(name) = LOWER($2)
LIMIT
1
`
@ -187,6 +214,32 @@ func (q *sqlQuerier) GetProjectByOrganizationAndName(ctx context.Context, arg Ge
return i, err
}
const getProjectHistoryByID = `-- name: GetProjectHistoryByID :one
SELECT
id, project_id, created_at, updated_at, name, description, storage_method, storage_source, import_job_id
FROM
project_history
WHERE
id = $1
`
func (q *sqlQuerier) GetProjectHistoryByID(ctx context.Context, id uuid.UUID) (ProjectHistory, error) {
row := q.db.QueryRowContext(ctx, getProjectHistoryByID, id)
var i ProjectHistory
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Description,
&i.StorageMethod,
&i.StorageSource,
&i.ImportJobID,
)
return i, err
}
const getProjectHistoryByProjectID = `-- name: GetProjectHistoryByProjectID :many
SELECT
id, project_id, created_at, updated_at, name, description, storage_method, storage_source, import_job_id
@ -275,7 +328,7 @@ SELECT
FROM
users
WHERE
username = $1
LOWER(username) = LOWER($1)
OR email = $2
LIMIT
1
@ -365,6 +418,275 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) {
return count, err
}
const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many
SELECT
id, workspace_resource_id, created_at, updated_at, instance_metadata, resource_metadata
FROM
workspace_agent
WHERE
workspace_resource_id = ANY($1 :: uuid [ ])
`
func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByResourceIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgent
for rows.Next() {
var i WorkspaceAgent
if err := rows.Scan(
&i.ID,
&i.WorkspaceResourceID,
&i.CreatedAt,
&i.UpdatedAt,
&i.InstanceMetadata,
&i.ResourceMetadata,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceByUserIDAndName = `-- name: GetWorkspaceByUserIDAndName :one
SELECT
id, created_at, updated_at, owner_id, project_id, name
FROM
workspace
WHERE
owner_id = $1
AND LOWER(name) = LOWER($2)
`
type GetWorkspaceByUserIDAndNameParams struct {
OwnerID string `db:"owner_id" json:"owner_id"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) GetWorkspaceByUserIDAndName(ctx context.Context, arg GetWorkspaceByUserIDAndNameParams) (Workspace, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceByUserIDAndName, arg.OwnerID, arg.Name)
var i Workspace
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.ProjectID,
&i.Name,
)
return i, err
}
const getWorkspaceHistoryByWorkspaceID = `-- name: GetWorkspaceHistoryByWorkspaceID :many
SELECT
id, created_at, updated_at, completed_at, workspace_id, project_history_id, before_id, after_id, transition, initiator, provisioner_state, provision_job_id
FROM
workspace_history
WHERE
workspace_id = $1
`
func (q *sqlQuerier) GetWorkspaceHistoryByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceHistory, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceHistoryByWorkspaceID, workspaceID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceHistory
for rows.Next() {
var i WorkspaceHistory
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.CompletedAt,
&i.WorkspaceID,
&i.ProjectHistoryID,
&i.BeforeID,
&i.AfterID,
&i.Transition,
&i.Initiator,
&i.ProvisionerState,
&i.ProvisionJobID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceHistoryByWorkspaceIDWithoutAfter = `-- name: GetWorkspaceHistoryByWorkspaceIDWithoutAfter :one
SELECT
id, created_at, updated_at, completed_at, workspace_id, project_history_id, before_id, after_id, transition, initiator, provisioner_state, provision_job_id
FROM
workspace_history
WHERE
workspace_id = $1
AND after_id IS NULL
LIMIT
1
`
func (q *sqlQuerier) GetWorkspaceHistoryByWorkspaceIDWithoutAfter(ctx context.Context, workspaceID uuid.UUID) (WorkspaceHistory, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceHistoryByWorkspaceIDWithoutAfter, workspaceID)
var i WorkspaceHistory
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.CompletedAt,
&i.WorkspaceID,
&i.ProjectHistoryID,
&i.BeforeID,
&i.AfterID,
&i.Transition,
&i.Initiator,
&i.ProvisionerState,
&i.ProvisionJobID,
)
return i, err
}
const getWorkspaceResourcesByHistoryID = `-- name: GetWorkspaceResourcesByHistoryID :many
SELECT
id, created_at, workspace_history_id, type, name, workspace_agent_token, workspace_agent_id
FROM
workspace_resource
WHERE
workspace_history_id = $1
`
func (q *sqlQuerier) GetWorkspaceResourcesByHistoryID(ctx context.Context, workspaceHistoryID uuid.UUID) ([]WorkspaceResource, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceResourcesByHistoryID, workspaceHistoryID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceResource
for rows.Next() {
var i WorkspaceResource
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.WorkspaceHistoryID,
&i.Type,
&i.Name,
&i.WorkspaceAgentToken,
&i.WorkspaceAgentID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspacesByProjectAndUserID = `-- name: GetWorkspacesByProjectAndUserID :many
SELECT
id, created_at, updated_at, owner_id, project_id, name
FROM
workspace
WHERE
owner_id = $1
AND project_id = $2
`
type GetWorkspacesByProjectAndUserIDParams struct {
OwnerID string `db:"owner_id" json:"owner_id"`
ProjectID uuid.UUID `db:"project_id" json:"project_id"`
}
func (q *sqlQuerier) GetWorkspacesByProjectAndUserID(ctx context.Context, arg GetWorkspacesByProjectAndUserIDParams) ([]Workspace, error) {
rows, err := q.db.QueryContext(ctx, getWorkspacesByProjectAndUserID, arg.OwnerID, arg.ProjectID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Workspace
for rows.Next() {
var i Workspace
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.ProjectID,
&i.Name,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspacesByUserID = `-- name: GetWorkspacesByUserID :many
SELECT
id, created_at, updated_at, owner_id, project_id, name
FROM
workspace
WHERE
owner_id = $1
`
func (q *sqlQuerier) GetWorkspacesByUserID(ctx context.Context, ownerID string) ([]Workspace, error) {
rows, err := q.db.QueryContext(ctx, getWorkspacesByUserID, ownerID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Workspace
for rows.Next() {
var i Workspace
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.ProjectID,
&i.Name,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertAPIKey = `-- name: InsertAPIKey :one
INSERT INTO
api_keys (
@ -801,6 +1123,198 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
return i, err
}
const insertWorkspace = `-- name: InsertWorkspace :one
INSERT INTO
workspace (
id,
created_at,
updated_at,
owner_id,
project_id,
name
)
VALUES
($1, $2, $3, $4, $5, $6) RETURNING id, created_at, updated_at, owner_id, project_id, name
`
type InsertWorkspaceParams 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"`
OwnerID string `db:"owner_id" json:"owner_id"`
ProjectID uuid.UUID `db:"project_id" json:"project_id"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error) {
row := q.db.QueryRowContext(ctx, insertWorkspace,
arg.ID,
arg.CreatedAt,
arg.UpdatedAt,
arg.OwnerID,
arg.ProjectID,
arg.Name,
)
var i Workspace
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.ProjectID,
&i.Name,
)
return i, err
}
const insertWorkspaceAgent = `-- name: InsertWorkspaceAgent :one
INSERT INTO
workspace_agent (
id,
workspace_resource_id,
created_at,
updated_at,
instance_metadata,
resource_metadata
)
VALUES
($1, $2, $3, $4, $5, $6) RETURNING id, workspace_resource_id, created_at, updated_at, instance_metadata, resource_metadata
`
type InsertWorkspaceAgentParams struct {
ID uuid.UUID `db:"id" json:"id"`
WorkspaceResourceID uuid.UUID `db:"workspace_resource_id" json:"workspace_resource_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
InstanceMetadata json.RawMessage `db:"instance_metadata" json:"instance_metadata"`
ResourceMetadata json.RawMessage `db:"resource_metadata" json:"resource_metadata"`
}
func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) {
row := q.db.QueryRowContext(ctx, insertWorkspaceAgent,
arg.ID,
arg.WorkspaceResourceID,
arg.CreatedAt,
arg.UpdatedAt,
arg.InstanceMetadata,
arg.ResourceMetadata,
)
var i WorkspaceAgent
err := row.Scan(
&i.ID,
&i.WorkspaceResourceID,
&i.CreatedAt,
&i.UpdatedAt,
&i.InstanceMetadata,
&i.ResourceMetadata,
)
return i, err
}
const insertWorkspaceHistory = `-- name: InsertWorkspaceHistory :one
INSERT INTO
workspace_history (
id,
created_at,
updated_at,
workspace_id,
project_history_id,
before_id,
transition,
initiator,
provision_job_id
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, created_at, updated_at, completed_at, workspace_id, project_history_id, before_id, after_id, transition, initiator, provisioner_state, provision_job_id
`
type InsertWorkspaceHistoryParams 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"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
ProjectHistoryID uuid.UUID `db:"project_history_id" json:"project_history_id"`
BeforeID uuid.NullUUID `db:"before_id" json:"before_id"`
Transition WorkspaceTransition `db:"transition" json:"transition"`
Initiator string `db:"initiator" json:"initiator"`
ProvisionJobID uuid.UUID `db:"provision_job_id" json:"provision_job_id"`
}
func (q *sqlQuerier) InsertWorkspaceHistory(ctx context.Context, arg InsertWorkspaceHistoryParams) (WorkspaceHistory, error) {
row := q.db.QueryRowContext(ctx, insertWorkspaceHistory,
arg.ID,
arg.CreatedAt,
arg.UpdatedAt,
arg.WorkspaceID,
arg.ProjectHistoryID,
arg.BeforeID,
arg.Transition,
arg.Initiator,
arg.ProvisionJobID,
)
var i WorkspaceHistory
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.CompletedAt,
&i.WorkspaceID,
&i.ProjectHistoryID,
&i.BeforeID,
&i.AfterID,
&i.Transition,
&i.Initiator,
&i.ProvisionerState,
&i.ProvisionJobID,
)
return i, err
}
const insertWorkspaceResource = `-- name: InsertWorkspaceResource :one
INSERT INTO
workspace_resource (
id,
created_at,
workspace_history_id,
type,
name,
workspace_agent_token
)
VALUES
($1, $2, $3, $4, $5, $6) RETURNING id, created_at, workspace_history_id, type, name, workspace_agent_token, workspace_agent_id
`
type InsertWorkspaceResourceParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
WorkspaceHistoryID uuid.UUID `db:"workspace_history_id" json:"workspace_history_id"`
Type string `db:"type" json:"type"`
Name string `db:"name" json:"name"`
WorkspaceAgentToken string `db:"workspace_agent_token" json:"workspace_agent_token"`
}
func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) {
row := q.db.QueryRowContext(ctx, insertWorkspaceResource,
arg.ID,
arg.CreatedAt,
arg.WorkspaceHistoryID,
arg.Type,
arg.Name,
arg.WorkspaceAgentToken,
)
var i WorkspaceResource
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.WorkspaceHistoryID,
&i.Type,
&i.Name,
&i.WorkspaceAgentToken,
&i.WorkspaceAgentID,
)
return i, err
}
const updateAPIKeyByID = `-- name: UpdateAPIKeyByID :exec
UPDATE
api_keys
@ -834,3 +1348,24 @@ func (q *sqlQuerier) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDP
)
return err
}
const updateWorkspaceHistoryByID = `-- name: UpdateWorkspaceHistoryByID :exec
UPDATE
workspace_history
SET
updated_at = $2,
after_id = $3
WHERE
id = $1
`
type UpdateWorkspaceHistoryByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
AfterID uuid.NullUUID `db:"after_id" json:"after_id"`
}
func (q *sqlQuerier) UpdateWorkspaceHistoryByID(ctx context.Context, arg UpdateWorkspaceHistoryByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceHistoryByID, arg.ID, arg.UpdatedAt, arg.AfterID)
return err
}

View File

@ -15,7 +15,7 @@ import (
type projectParamContextKey struct{}
// ProjectParam returns the project from the ExtractProjectParameter handler.
// ProjectParam returns the project from the ExtractProjectParam handler.
func ProjectParam(r *http.Request) database.Project {
project, ok := r.Context().Value(projectParamContextKey{}).(database.Project)
if !ok {
@ -24,8 +24,8 @@ func ProjectParam(r *http.Request) database.Project {
return project
}
// ExtractProjectParameter grabs a project from the "project" URL parameter.
func ExtractProjectParameter(db database.Store) func(http.Handler) http.Handler {
// ExtractProjectParam grabs a project from the "project" URL parameter.
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)

View File

@ -86,7 +86,7 @@ func TestProjectParam(t *testing.T) {
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractProjectParameter(db),
httpmw.ExtractProjectParam(db),
)
rtr.Get("/", nil)
r, _ := setupAuthentication(db)
@ -105,7 +105,7 @@ func TestProjectParam(t *testing.T) {
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractProjectParameter(db),
httpmw.ExtractProjectParam(db),
)
rtr.Get("/", nil)
@ -126,7 +126,7 @@ func TestProjectParam(t *testing.T) {
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractProjectParameter(db),
httpmw.ExtractProjectParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.ProjectParam(r)

View File

@ -33,13 +33,13 @@ func ExtractUserParam(db database.Store) func(http.Handler) http.Handler {
})
return
}
if userID != "me" {
apiKey := APIKey(r)
if apiKey.UserID != userID && userID != "me" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "getting non-personal users isn't supported yet",
})
return
}
apiKey := APIKey(r)
user, err := db.GetUserByID(r.Context(), apiKey.UserID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{

60
httpmw/workspaceparam.go Normal file
View File

@ -0,0 +1,60 @@
package httpmw
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/go-chi/chi"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
)
type workspaceParamContextKey struct{}
// WorkspaceParam returns the workspace from the ExtractWorkspaceParam handler.
func WorkspaceParam(r *http.Request) database.Workspace {
workspace, ok := r.Context().Value(workspaceParamContextKey{}).(database.Workspace)
if !ok {
panic("developer error: workspace param middleware not provided")
}
return workspace
}
// ExtractWorkspaceParam grabs a workspace from the "workspace" URL parameter.
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",
})
return
}
workspace, err := db.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("workspace %q does not exist", workspace),
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace: %s", err.Error()),
})
return
}
ctx := context.WithValue(r.Context(), workspaceParamContextKey{}, workspace)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}

View File

@ -0,0 +1,132 @@
package httpmw_test
import (
"context"
"crypto/sha256"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cryptorand"
"github.com/coder/coder/database"
"github.com/coder/coder/database/databasefake"
"github.com/coder/coder/httpmw"
)
func TestWorkspaceParam(t *testing.T) {
t.Parallel()
setup := func(db database.Store) (*http.Request, database.User) {
var (
id, secret = randomAPIKeyParts()
hashed = sha256.Sum256([]byte(secret))
)
r := httptest.NewRequest("GET", "/", nil)
r.AddCookie(&http.Cookie{
Name: httpmw.AuthCookie,
Value: fmt.Sprintf("%s-%s", id, secret),
})
userID, err := cryptorand.String(16)
require.NoError(t, err)
username, err := cryptorand.String(8)
require.NoError(t, err)
user, err := db.InsertUser(r.Context(), database.InsertUserParams{
ID: userID,
Email: "testaccount@coder.com",
Name: "example",
LoginType: database.LoginTypeBuiltIn,
HashedPassword: hashed[:],
Username: username,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
require.NoError(t, err)
_, err = db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: id,
UserID: user.ID,
HashedSecret: hashed[:],
LastUsed: database.Now(),
ExpiresAt: database.Now().Add(time.Minute),
})
require.NoError(t, err)
ctx := chi.NewRouteContext()
ctx.URLParams.Add("user", "me")
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx))
return r, user
}
t.Run("None", 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("/", 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("NotFound", 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("/", nil)
r, _ := setup(db)
chi.RouteContext(r.Context()).URLParams.Add("workspace", "frog")
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusNotFound, 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) {
_ = httpmw.WorkspaceParam(r)
rw.WriteHeader(http.StatusOK)
})
r, user := setup(db)
workspace, err := db.InsertWorkspace(context.Background(), database.InsertWorkspaceParams{
ID: uuid.New(),
OwnerID: user.ID,
Name: "hello",
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("workspace", workspace.Name)
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusOK, res.StatusCode)
})
}