mirror of https://github.com/coder/coder.git
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:
parent
139828d594
commit
5b01f615eb
|
@ -16,7 +16,7 @@ coverage:
|
|||
status:
|
||||
project:
|
||||
default:
|
||||
target: 75%
|
||||
target: 70%
|
||||
informational: yes
|
||||
|
||||
ignore:
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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{},
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue