Skip to content

Commit

Permalink
refactor: Move all HTTP routes to top-level struct (#130)
Browse files Browse the repository at this point in the history
* feat: Add history middleware parameters

These will be used for streaming logs, checking status,
and other operations related to workspace and project
history.

* refactor: Move all HTTP routes to top-level struct

Nesting all structs behind their respective structures
is leaky, and promotes naming conflicts between handlers.

Our HTTP routes cannot have conflicts, so neither should
function naming.
  • Loading branch information
kylecarbs committed Feb 1, 2022
1 parent c5f1a37 commit 177eba8
Show file tree
Hide file tree
Showing 14 changed files with 623 additions and 552 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
"cmd": "make gen"
}
]
}
},
"cSpell.words": ["coderd", "coderdtest", "codersdk", "httpmw", "oneof", "stretchr", "xerrors"]
}
57 changes: 30 additions & 27 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,14 @@ import (
type Options struct {
Logger slog.Logger
Database database.Store
Pubsub database.Pubsub
}

// New constructs the Coder API into an HTTP handler.
func New(options *Options) http.Handler {
projects := &projects{
Database: options.Database,
}
users := &users{
Database: options.Database,
}
workspaces := &workspaces{
api := &api{
Database: options.Database,
Pubsub: options.Pubsub,
}

r := chi.NewRouter()
Expand All @@ -37,38 +33,38 @@ func New(options *Options) http.Handler {
Message: "👋",
})
})
r.Post("/login", users.loginWithPassword)
r.Post("/logout", users.logout)
r.Post("/login", api.postLogin)
r.Post("/logout", api.postLogout)
// Used for setup.
r.Post("/user", users.createInitialUser)
r.Post("/user", api.postUser)
r.Route("/users", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
)
r.Post("/", users.createUser)
r.Post("/", api.postUsers)
r.Group(func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/{user}", users.user)
r.Get("/{user}/organizations", users.userOrganizations)
r.Get("/{user}", api.userByName)
r.Get("/{user}/organizations", api.organizationsByUser)
})
})
r.Route("/projects", func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
)
r.Get("/", projects.allProjects)
r.Get("/", api.projects)
r.Route("/{organization}", func(r chi.Router) {
r.Use(httpmw.ExtractOrganizationParam(options.Database))
r.Get("/", projects.allProjectsForOrganization)
r.Post("/", projects.createProject)
r.Get("/", api.projectsByOrganization)
r.Post("/", api.postProjectsByOrganization)
r.Route("/{project}", func(r chi.Router) {
r.Use(httpmw.ExtractProjectParam(options.Database))
r.Get("/", projects.project)
r.Get("/", api.projectByOrganization)
r.Get("/workspaces", api.workspacesByProject)
r.Route("/history", func(r chi.Router) {
r.Get("/", projects.allProjectHistory)
r.Post("/", projects.createProjectHistory)
r.Get("/", api.projectHistoryByOrganization)
r.Post("/", api.postProjectHistoryByOrganization)
})
r.Get("/workspaces", workspaces.allWorkspacesForProject)
})
})
})
Expand All @@ -77,18 +73,18 @@ func New(options *Options) http.Handler {
// 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.Get("/", api.workspaces)
r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/", workspaces.listAllWorkspaces)
r.Post("/", workspaces.createWorkspaceForUser)
r.Get("/", api.workspaces)
r.Post("/", api.postWorkspaceByUser)
r.Route("/{workspace}", func(r chi.Router) {
r.Use(httpmw.ExtractWorkspaceParam(options.Database))
r.Get("/", workspaces.singleWorkspace)
r.Get("/", api.workspaceByUser)
r.Route("/history", func(r chi.Router) {
r.Post("/", workspaces.createWorkspaceHistory)
r.Get("/", workspaces.listAllWorkspaceHistory)
r.Get("/latest", workspaces.latestWorkspaceHistory)
r.Post("/", api.postWorkspaceHistoryByUser)
r.Get("/", api.workspaceHistoryByUser)
r.Get("/latest", api.latestWorkspaceHistoryByUser)
})
})
})
Expand All @@ -97,3 +93,10 @@ func New(options *Options) http.Handler {
r.NotFound(site.Handler().ServeHTTP)
return r
}

// API contains all route handlers. Only HTTP handlers should
// be added to this struct for code clarity.
type api struct {
Database database.Store
Pubsub database.Pubsub
}
118 changes: 118 additions & 0 deletions coderd/projecthistory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package coderd

import (
"archive/tar"
"bytes"
"database/sql"
"errors"
"fmt"
"net/http"
"time"

"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"

"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
"github.com/coder/coder/httpmw"
)

// 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"`
UpdatedAt time.Time `json:"updated_at"`
Name string `json:"name"`
StorageMethod database.ProjectStorageMethod `json:"storage_method"`
}

// CreateProjectHistoryRequest enables callers to create a new Project Version.
type CreateProjectHistoryRequest struct {
StorageMethod database.ProjectStorageMethod `json:"storage_method" validate:"oneof=inline-archive,required"`
StorageSource []byte `json:"storage_source" validate:"max=1048576,required"`
}

// Lists history for a single project.
func (api *api) projectHistoryByOrganization(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)

history, err := api.Database.GetProjectHistoryByProjectID(r.Context(), project.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project history: %s", err),
})
return
}
apiHistory := make([]ProjectHistory, 0)
for _, version := range history {
apiHistory = append(apiHistory, convertProjectHistory(version))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiHistory)
}

// Creates a new version of the project. An import job is queued to parse
// the storage method provided. Once completed, the import job will specify
// the version as latest.
func (api *api) postProjectHistoryByOrganization(rw http.ResponseWriter, r *http.Request) {
var createProjectVersion CreateProjectHistoryRequest
if !httpapi.Read(rw, r, &createProjectVersion) {
return
}

switch createProjectVersion.StorageMethod {
case database.ProjectStorageMethodInlineArchive:
tarReader := tar.NewReader(bytes.NewReader(createProjectVersion.StorageSource))
_, err := tarReader.Next()
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "the archive must be a tar",
})
return
}
default:
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("unsupported storage method %s", createProjectVersion.StorageMethod),
})
return
}

project := httpmw.ProjectParam(r)
history, err := api.Database.InsertProjectHistory(r.Context(), database.InsertProjectHistoryParams{
ID: uuid.New(),
ProjectID: project.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
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{
Message: fmt.Sprintf("insert project history: %s", err),
})
return
}

// TODO: A job to process the new version should occur here.

render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertProjectHistory(history))
}

func convertProjectHistory(history database.ProjectHistory) ProjectHistory {
return ProjectHistory{
ID: history.ID,
ProjectID: history.ProjectID,
CreatedAt: history.CreatedAt,
UpdatedAt: history.UpdatedAt,
Name: history.Name,
}
}
101 changes: 101 additions & 0 deletions coderd/projecthistory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package coderd_test

import (
"archive/tar"
"bytes"
"context"
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/database"
)

func TestProjectHistory(t *testing.T) {
t.Parallel()

t.Run("NoHistory", 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: "someproject",
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
versions, err := server.Client.ProjectHistory(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.Len(t, versions, 0)
})

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: "someproject",
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
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)
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectHistoryRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: buffer.Bytes(),
})
require.NoError(t, err)
versions, err := server.Client.ProjectHistory(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.Len(t, versions, 1)
})

t.Run("CreateHistoryArchiveTooBig", 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: "someproject",
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
var buffer bytes.Buffer
writer := tar.NewWriter(&buffer)
err = writer.WriteHeader(&tar.Header{
Name: "file",
Size: 1 << 21,
})
require.NoError(t, err)
_, err = writer.Write(make([]byte, 1<<21))
require.NoError(t, err)
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectHistoryRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: buffer.Bytes(),
})
require.Error(t, err)
})

t.Run("CreateHistoryInvalidArchive", 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: "someproject",
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectHistoryRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: []byte{},
})
require.Error(t, err)
})
}

0 comments on commit 177eba8

Please sign in to comment.