Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7a9f714

Browse files
committed
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.
1 parent ea69016 commit 7a9f714

14 files changed

+627
-554
lines changed

.vscode/settings.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,14 @@
2222
"cmd": "make gen"
2323
}
2424
]
25-
}
25+
},
26+
"cSpell.words": [
27+
"coderd",
28+
"coderdtest",
29+
"codersdk",
30+
"httpmw",
31+
"oneof",
32+
"stretchr",
33+
"xerrors"
34+
]
2635
}

coderd/coderd.go

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,14 @@ import (
1616
type Options struct {
1717
Logger slog.Logger
1818
Database database.Store
19+
Pubsub database.Pubsub
1920
}
2021

2122
// New constructs the Coder API into an HTTP handler.
2223
func New(options *Options) http.Handler {
23-
projects := &projects{
24-
Database: options.Database,
25-
}
26-
users := &users{
27-
Database: options.Database,
28-
}
29-
workspaces := &workspaces{
24+
api := &api{
3025
Database: options.Database,
26+
Pubsub: options.Pubsub,
3127
}
3228

3329
r := chi.NewRouter()
@@ -37,38 +33,38 @@ func New(options *Options) http.Handler {
3733
Message: "👋",
3834
})
3935
})
40-
r.Post("/login", users.loginWithPassword)
41-
r.Post("/logout", users.logout)
36+
r.Post("/login", api.postLogin)
37+
r.Post("/logout", api.postLogout)
4238
// Used for setup.
43-
r.Post("/user", users.createInitialUser)
39+
r.Post("/user", api.postUser)
4440
r.Route("/users", func(r chi.Router) {
4541
r.Use(
4642
httpmw.ExtractAPIKey(options.Database, nil),
4743
)
48-
r.Post("/", users.createUser)
44+
r.Post("/", api.postUsers)
4945
r.Group(func(r chi.Router) {
5046
r.Use(httpmw.ExtractUserParam(options.Database))
51-
r.Get("/{user}", users.user)
52-
r.Get("/{user}/organizations", users.userOrganizations)
47+
r.Get("/{user}", api.userByName)
48+
r.Get("/{user}/organizations", api.organizationsByUser)
5349
})
5450
})
5551
r.Route("/projects", func(r chi.Router) {
5652
r.Use(
5753
httpmw.ExtractAPIKey(options.Database, nil),
5854
)
59-
r.Get("/", projects.allProjects)
55+
r.Get("/", api.projects)
6056
r.Route("/{organization}", func(r chi.Router) {
6157
r.Use(httpmw.ExtractOrganizationParam(options.Database))
62-
r.Get("/", projects.allProjectsForOrganization)
63-
r.Post("/", projects.createProject)
58+
r.Get("/", api.projectsByOrganization)
59+
r.Post("/", api.postProjectsByOrganization)
6460
r.Route("/{project}", func(r chi.Router) {
6561
r.Use(httpmw.ExtractProjectParam(options.Database))
66-
r.Get("/", projects.project)
62+
r.Get("/", api.projectByOrganization)
63+
r.Get("/workspaces", api.workspacesByProject)
6764
r.Route("/history", func(r chi.Router) {
68-
r.Get("/", projects.allProjectHistory)
69-
r.Post("/", projects.createProjectHistory)
65+
r.Get("/", api.projectHistoryByOrganization)
66+
r.Post("/", api.postProjectHistoryByOrganization)
7067
})
71-
r.Get("/workspaces", workspaces.allWorkspacesForProject)
7268
})
7369
})
7470
})
@@ -77,18 +73,18 @@ func New(options *Options) http.Handler {
7773
// their respective routes. eg. /orgs/<name>/workspaces
7874
r.Route("/workspaces", func(r chi.Router) {
7975
r.Use(httpmw.ExtractAPIKey(options.Database, nil))
80-
r.Get("/", workspaces.listAllWorkspaces)
76+
r.Get("/", api.workspaces)
8177
r.Route("/{user}", func(r chi.Router) {
8278
r.Use(httpmw.ExtractUserParam(options.Database))
83-
r.Get("/", workspaces.listAllWorkspaces)
84-
r.Post("/", workspaces.createWorkspaceForUser)
79+
r.Get("/", api.workspaces)
80+
r.Post("/", api.postWorkspaceByUser)
8581
r.Route("/{workspace}", func(r chi.Router) {
8682
r.Use(httpmw.ExtractWorkspaceParam(options.Database))
87-
r.Get("/", workspaces.singleWorkspace)
83+
r.Get("/", api.workspaceByUser)
8884
r.Route("/history", func(r chi.Router) {
89-
r.Post("/", workspaces.createWorkspaceHistory)
90-
r.Get("/", workspaces.listAllWorkspaceHistory)
91-
r.Get("/latest", workspaces.latestWorkspaceHistory)
85+
r.Post("/", api.postWorkspaceHistoryByUser)
86+
r.Get("/", api.workspaceHistoryByUser)
87+
r.Get("/latest", api.latestWorkspaceHistoryByUser)
9288
})
9389
})
9490
})
@@ -97,3 +93,10 @@ func New(options *Options) http.Handler {
9793
r.NotFound(site.Handler().ServeHTTP)
9894
return r
9995
}
96+
97+
// API contains all route handlers. Only HTTP handlers should
98+
// be added to this struct for code clarity.
99+
type api struct {
100+
Database database.Store
101+
Pubsub database.Pubsub
102+
}

coderd/projecthistory.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package coderd
2+
3+
import (
4+
"archive/tar"
5+
"bytes"
6+
"database/sql"
7+
"errors"
8+
"fmt"
9+
"net/http"
10+
"time"
11+
12+
"github.com/go-chi/render"
13+
"github.com/google/uuid"
14+
"github.com/moby/moby/pkg/namesgenerator"
15+
16+
"github.com/coder/coder/database"
17+
"github.com/coder/coder/httpapi"
18+
"github.com/coder/coder/httpmw"
19+
)
20+
21+
// ProjectHistory is the JSON representation of Coder project version history.
22+
type ProjectHistory struct {
23+
ID uuid.UUID `json:"id"`
24+
ProjectID uuid.UUID `json:"project_id"`
25+
CreatedAt time.Time `json:"created_at"`
26+
UpdatedAt time.Time `json:"updated_at"`
27+
Name string `json:"name"`
28+
StorageMethod database.ProjectStorageMethod `json:"storage_method"`
29+
}
30+
31+
// CreateProjectHistoryRequest enables callers to create a new Project Version.
32+
type CreateProjectHistoryRequest struct {
33+
StorageMethod database.ProjectStorageMethod `json:"storage_method" validate:"oneof=inline-archive,required"`
34+
StorageSource []byte `json:"storage_source" validate:"max=1048576,required"`
35+
}
36+
37+
// Lists history for a single project.
38+
func (api *api) projectHistoryByOrganization(rw http.ResponseWriter, r *http.Request) {
39+
project := httpmw.ProjectParam(r)
40+
41+
history, err := api.Database.GetProjectHistoryByProjectID(r.Context(), project.ID)
42+
if errors.Is(err, sql.ErrNoRows) {
43+
err = nil
44+
}
45+
if err != nil {
46+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
47+
Message: fmt.Sprintf("get project history: %s", err),
48+
})
49+
return
50+
}
51+
apiHistory := make([]ProjectHistory, 0)
52+
for _, version := range history {
53+
apiHistory = append(apiHistory, convertProjectHistory(version))
54+
}
55+
render.Status(r, http.StatusOK)
56+
render.JSON(rw, r, apiHistory)
57+
}
58+
59+
// Creates a new version of the project. An import job is queued to parse
60+
// the storage method provided. Once completed, the import job will specify
61+
// the version as latest.
62+
func (api *api) postProjectHistoryByOrganization(rw http.ResponseWriter, r *http.Request) {
63+
var createProjectVersion CreateProjectHistoryRequest
64+
if !httpapi.Read(rw, r, &createProjectVersion) {
65+
return
66+
}
67+
68+
switch createProjectVersion.StorageMethod {
69+
case database.ProjectStorageMethodInlineArchive:
70+
tarReader := tar.NewReader(bytes.NewReader(createProjectVersion.StorageSource))
71+
_, err := tarReader.Next()
72+
if err != nil {
73+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
74+
Message: "the archive must be a tar",
75+
})
76+
return
77+
}
78+
default:
79+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
80+
Message: fmt.Sprintf("unsupported storage method %s", createProjectVersion.StorageMethod),
81+
})
82+
return
83+
}
84+
85+
project := httpmw.ProjectParam(r)
86+
history, err := api.Database.InsertProjectHistory(r.Context(), database.InsertProjectHistoryParams{
87+
ID: uuid.New(),
88+
ProjectID: project.ID,
89+
CreatedAt: database.Now(),
90+
UpdatedAt: database.Now(),
91+
Name: namesgenerator.GetRandomName(1),
92+
StorageMethod: createProjectVersion.StorageMethod,
93+
StorageSource: createProjectVersion.StorageSource,
94+
// TODO: Make this do something!
95+
ImportJobID: uuid.New(),
96+
})
97+
if err != nil {
98+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
99+
Message: fmt.Sprintf("insert project history: %s", err),
100+
})
101+
return
102+
}
103+
104+
// TODO: A job to process the new version should occur here.
105+
106+
render.Status(r, http.StatusCreated)
107+
render.JSON(rw, r, convertProjectHistory(history))
108+
}
109+
110+
func convertProjectHistory(history database.ProjectHistory) ProjectHistory {
111+
return ProjectHistory{
112+
ID: history.ID,
113+
ProjectID: history.ProjectID,
114+
CreatedAt: history.CreatedAt,
115+
UpdatedAt: history.UpdatedAt,
116+
Name: history.Name,
117+
}
118+
}

coderd/projecthistory_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package coderd_test
2+
3+
import (
4+
"archive/tar"
5+
"bytes"
6+
"context"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/coder/coder/coderd"
12+
"github.com/coder/coder/coderd/coderdtest"
13+
"github.com/coder/coder/database"
14+
)
15+
16+
func TestProjectHistory(t *testing.T) {
17+
t.Run("NoHistory", func(t *testing.T) {
18+
t.Parallel()
19+
server := coderdtest.New(t)
20+
user := server.RandomInitialUser(t)
21+
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
22+
Name: "someproject",
23+
Provisioner: database.ProvisionerTypeTerraform,
24+
})
25+
require.NoError(t, err)
26+
versions, err := server.Client.ProjectHistory(context.Background(), user.Organization, project.Name)
27+
require.NoError(t, err)
28+
require.Len(t, versions, 0)
29+
})
30+
31+
t.Run("CreateHistory", func(t *testing.T) {
32+
t.Parallel()
33+
server := coderdtest.New(t)
34+
user := server.RandomInitialUser(t)
35+
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
36+
Name: "someproject",
37+
Provisioner: database.ProvisionerTypeTerraform,
38+
})
39+
require.NoError(t, err)
40+
var buffer bytes.Buffer
41+
writer := tar.NewWriter(&buffer)
42+
err = writer.WriteHeader(&tar.Header{
43+
Name: "file",
44+
Size: 1 << 10,
45+
})
46+
require.NoError(t, err)
47+
_, err = writer.Write(make([]byte, 1<<10))
48+
require.NoError(t, err)
49+
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectHistoryRequest{
50+
StorageMethod: database.ProjectStorageMethodInlineArchive,
51+
StorageSource: buffer.Bytes(),
52+
})
53+
require.NoError(t, err)
54+
versions, err := server.Client.ProjectHistory(context.Background(), user.Organization, project.Name)
55+
require.NoError(t, err)
56+
require.Len(t, versions, 1)
57+
})
58+
59+
t.Run("CreateHistoryArchiveTooBig", func(t *testing.T) {
60+
t.Parallel()
61+
server := coderdtest.New(t)
62+
user := server.RandomInitialUser(t)
63+
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
64+
Name: "someproject",
65+
Provisioner: database.ProvisionerTypeTerraform,
66+
})
67+
require.NoError(t, err)
68+
var buffer bytes.Buffer
69+
writer := tar.NewWriter(&buffer)
70+
err = writer.WriteHeader(&tar.Header{
71+
Name: "file",
72+
Size: 1 << 21,
73+
})
74+
require.NoError(t, err)
75+
_, err = writer.Write(make([]byte, 1<<21))
76+
require.NoError(t, err)
77+
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectHistoryRequest{
78+
StorageMethod: database.ProjectStorageMethodInlineArchive,
79+
StorageSource: buffer.Bytes(),
80+
})
81+
require.Error(t, err)
82+
})
83+
84+
t.Run("CreateHistoryInvalidArchive", func(t *testing.T) {
85+
t.Parallel()
86+
server := coderdtest.New(t)
87+
user := server.RandomInitialUser(t)
88+
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
89+
Name: "someproject",
90+
Provisioner: database.ProvisionerTypeTerraform,
91+
})
92+
require.NoError(t, err)
93+
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectHistoryRequest{
94+
StorageMethod: database.ProjectStorageMethodInlineArchive,
95+
StorageSource: []byte{},
96+
})
97+
require.Error(t, err)
98+
})
99+
}

0 commit comments

Comments
 (0)