mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Refactor API routes to use UUIDs instead of friendly names (#401)
* Add client for agent * Cleanup code * Fix linting error * Rename routes to be simpler * Rename workspace history to workspace build * Refactor HTTP middlewares to use UUIDs * Cleanup routes * Compiles! * Fix files and organizations * Fix querying * Fix agent lock * Cleanup database abstraction * Add parameters * Fix linting errors * Fix log race * Lock on close wait * Fix log cleanup * Fix e2e tests * Fix upstream version of opencensus-go * Update coderdtest.go * Fix coverpkg * Fix codecov ignore
This commit is contained in:
@@ -27,6 +27,7 @@ func randomAPIKeyParts() (id string, secret string) {
|
||||
|
||||
func TestAPIKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
successHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Only called if the API key passes through the handler.
|
||||
httpapi.Write(rw, http.StatusOK, httpapi.Response{
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package httpmw
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/httpapi"
|
||||
)
|
||||
|
||||
// parseUUID consumes a url parameter and parses it as a UUID.
|
||||
func parseUUID(rw http.ResponseWriter, r *http.Request, param string) (uuid.UUID, bool) {
|
||||
rawID := chi.URLParam(r, param)
|
||||
if rawID == "" {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: fmt.Sprintf("%s must be provided", param),
|
||||
})
|
||||
return uuid.UUID{}, false
|
||||
}
|
||||
parsed, err := uuid.Parse(rawID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: fmt.Sprintf("%s must be a uuid", param),
|
||||
})
|
||||
return uuid.UUID{}, false
|
||||
}
|
||||
return parsed, true
|
||||
}
|
||||
@@ -40,18 +40,17 @@ func OrganizationMemberParam(r *http.Request) database.OrganizationMember {
|
||||
func ExtractOrganizationParam(db database.Store) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
apiKey := APIKey(r)
|
||||
organizationName := chi.URLParam(r, "organization")
|
||||
if organizationName == "" {
|
||||
organizationID := chi.URLParam(r, "organization")
|
||||
if organizationID == "" {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: "organization name must be provided",
|
||||
Message: "organization must be provided",
|
||||
})
|
||||
return
|
||||
}
|
||||
organization, err := db.GetOrganizationByName(r.Context(), organizationName)
|
||||
organization, err := db.GetOrganizationByID(r.Context(), organizationID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("organization %q does not exist", organizationName),
|
||||
Message: fmt.Sprintf("organization %q does not exist", organizationID),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -61,6 +60,7 @@ func ExtractOrganizationParam(db database.Store) func(http.Handler) http.Handler
|
||||
})
|
||||
return
|
||||
}
|
||||
apiKey := APIKey(r)
|
||||
organizationMember, err := db.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
|
||||
OrganizationID: organization.ID,
|
||||
UserID: apiKey.UserID,
|
||||
|
||||
@@ -113,7 +113,7 @@ func TestOrganizationParam(t *testing.T) {
|
||||
UpdatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.Name)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.ID)
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractOrganizationParam(db),
|
||||
@@ -147,7 +147,7 @@ func TestOrganizationParam(t *testing.T) {
|
||||
UpdatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.Name)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.ID)
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractOrganizationParam(db),
|
||||
|
||||
+6
-13
@@ -28,32 +28,25 @@ func ProjectParam(r *http.Request) database.Project {
|
||||
func ExtractProjectParam(db database.Store) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
organization := OrganizationParam(r)
|
||||
projectName := chi.URLParam(r, "project")
|
||||
if projectName == "" {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: "project name must be provided",
|
||||
})
|
||||
projectID, parsed := parseUUID(rw, r, "project")
|
||||
if !parsed {
|
||||
return
|
||||
}
|
||||
project, err := db.GetProjectByOrganizationAndName(r.Context(), database.GetProjectByOrganizationAndNameParams{
|
||||
OrganizationID: organization.ID,
|
||||
Name: projectName,
|
||||
})
|
||||
project, err := db.GetProjectByID(r.Context(), projectID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("project %q does not exist", projectName),
|
||||
Message: fmt.Sprintf("project %q does not exist", projectID),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get project: %s", err.Error()),
|
||||
Message: fmt.Sprintf("get project: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), projectParamContextKey{}, project)
|
||||
chi.RouteContext(ctx).URLParams.Add("organization", project.OrganizationID)
|
||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
+22
-14
@@ -74,7 +74,6 @@ func TestProjectParam(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := chi.NewRouteContext()
|
||||
ctx.URLParams.Add("organization", organization.Name)
|
||||
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx))
|
||||
return r, organization
|
||||
}
|
||||
@@ -83,11 +82,7 @@ func TestProjectParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractOrganizationParam(db),
|
||||
httpmw.ExtractProjectParam(db),
|
||||
)
|
||||
rtr.Use(httpmw.ExtractProjectParam(db))
|
||||
rtr.Get("/", nil)
|
||||
r, _ := setupAuthentication(db)
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -102,15 +97,11 @@ func TestProjectParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractOrganizationParam(db),
|
||||
httpmw.ExtractProjectParam(db),
|
||||
)
|
||||
rtr.Use(httpmw.ExtractProjectParam(db))
|
||||
rtr.Get("/", nil)
|
||||
|
||||
r, _ := setupAuthentication(db)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("project", "nothin")
|
||||
chi.RouteContext(r.Context()).URLParams.Add("project", uuid.NewString())
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
@@ -119,14 +110,31 @@ func TestProjectParam(t *testing.T) {
|
||||
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BadUUID", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(httpmw.ExtractProjectParam(db))
|
||||
rtr.Get("/", nil)
|
||||
|
||||
r, _ := setupAuthentication(db)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("project", "not-a-uuid")
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Project", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractOrganizationParam(db),
|
||||
httpmw.ExtractProjectParam(db),
|
||||
httpmw.ExtractOrganizationParam(db),
|
||||
)
|
||||
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
_ = httpmw.ProjectParam(r)
|
||||
@@ -140,7 +148,7 @@ func TestProjectParam(t *testing.T) {
|
||||
Name: "moo",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("project", project.Name)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("project", project.ID.String())
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
@@ -29,27 +28,14 @@ func ProjectVersionParam(r *http.Request) database.ProjectVersion {
|
||||
func ExtractProjectVersionParam(db database.Store) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
project := ProjectParam(r)
|
||||
projectVersionName := chi.URLParam(r, "projectversion")
|
||||
if projectVersionName == "" {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: "project version name must be provided",
|
||||
})
|
||||
projectVersionID, parsed := parseUUID(rw, r, "projectversion")
|
||||
if !parsed {
|
||||
return
|
||||
}
|
||||
var projectVersion database.ProjectVersion
|
||||
uuid, err := uuid.Parse(projectVersionName)
|
||||
if err == nil {
|
||||
projectVersion, err = db.GetProjectVersionByID(r.Context(), uuid)
|
||||
} else {
|
||||
projectVersion, err = db.GetProjectVersionByProjectIDAndName(r.Context(), database.GetProjectVersionByProjectIDAndNameParams{
|
||||
ProjectID: project.ID,
|
||||
Name: projectVersionName,
|
||||
})
|
||||
}
|
||||
projectVersion, err := db.GetProjectVersionByID(r.Context(), projectVersionID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("project version %q does not exist", projectVersionName),
|
||||
Message: fmt.Sprintf("project version %q does not exist", projectVersionID),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -61,6 +47,7 @@ func ExtractProjectVersionParam(db database.Store) func(http.Handler) http.Handl
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), projectVersionParamContextKey{}, projectVersion)
|
||||
chi.RouteContext(ctx).URLParams.Add("organization", projectVersion.OrganizationID)
|
||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -90,12 +90,7 @@ func TestProjectVersionParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractOrganizationParam(db),
|
||||
httpmw.ExtractProjectParam(db),
|
||||
httpmw.ExtractProjectVersionParam(db),
|
||||
)
|
||||
rtr.Use(httpmw.ExtractProjectVersionParam(db))
|
||||
rtr.Get("/", nil)
|
||||
r, _ := setupAuthentication(db)
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -110,16 +105,11 @@ func TestProjectVersionParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractOrganizationParam(db),
|
||||
httpmw.ExtractProjectParam(db),
|
||||
httpmw.ExtractProjectVersionParam(db),
|
||||
)
|
||||
rtr.Use(httpmw.ExtractProjectVersionParam(db))
|
||||
rtr.Get("/", nil)
|
||||
|
||||
r, _ := setupAuthentication(db)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("projectversion", "nothin")
|
||||
chi.RouteContext(r.Context()).URLParams.Add("projectversion", uuid.NewString())
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
@@ -134,9 +124,8 @@ func TestProjectVersionParam(t *testing.T) {
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractOrganizationParam(db),
|
||||
httpmw.ExtractProjectParam(db),
|
||||
httpmw.ExtractProjectVersionParam(db),
|
||||
httpmw.ExtractOrganizationParam(db),
|
||||
)
|
||||
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
_ = httpmw.ProjectVersionParam(r)
|
||||
@@ -145,12 +134,12 @@ func TestProjectVersionParam(t *testing.T) {
|
||||
|
||||
r, project := setupAuthentication(db)
|
||||
projectVersion, err := db.InsertProjectVersion(context.Background(), database.InsertProjectVersionParams{
|
||||
ID: uuid.New(),
|
||||
ProjectID: project.ID,
|
||||
Name: "moo",
|
||||
ID: uuid.New(),
|
||||
OrganizationID: project.OrganizationID,
|
||||
Name: "moo",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("projectversion", projectVersion.Name)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("projectversion", projectVersion.ID.String())
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package httpmw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
)
|
||||
|
||||
type provisionerJobParamContextKey struct{}
|
||||
|
||||
// ProvisionerJobParam returns the project from the ExtractProjectParam handler.
|
||||
func ProvisionerJobParam(r *http.Request) database.ProvisionerJob {
|
||||
provisionerJob, ok := r.Context().Value(provisionerJobParamContextKey{}).(database.ProvisionerJob)
|
||||
if !ok {
|
||||
panic("developer error: provisioner job param middleware not provided")
|
||||
}
|
||||
return provisionerJob
|
||||
}
|
||||
|
||||
// ExtractProvisionerJobParam grabs a provisioner job from the "provisionerjob" URL parameter.
|
||||
func ExtractProvisionerJobParam(db database.Store) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
jobID := chi.URLParam(r, "provisionerjob")
|
||||
if jobID == "" {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: "provisioner job must be provided",
|
||||
})
|
||||
return
|
||||
}
|
||||
jobUUID, err := uuid.Parse(jobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: "job id must be a uuid",
|
||||
})
|
||||
return
|
||||
}
|
||||
job, err := db.GetProvisionerJobByID(r.Context(), jobUUID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: "job doesn't exist with that id",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), provisionerJobParamContextKey{}, job)
|
||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package httpmw_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/database/databasefake"
|
||||
"github.com/coder/coder/httpmw"
|
||||
)
|
||||
|
||||
func TestProvisionerJobParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
setup := func(db database.Store) (*http.Request, database.ProvisionerJob) {
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
provisionerJob, err := db.InsertProvisionerJob(context.Background(), database.InsertProvisionerJobParams{
|
||||
ID: uuid.New(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := chi.NewRouteContext()
|
||||
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx))
|
||||
return r, provisionerJob
|
||||
}
|
||||
|
||||
t.Run("None", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractProvisionerJobParam(db),
|
||||
)
|
||||
rtr.Get("/", nil)
|
||||
r, _ := setup(db)
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("BadUUID", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractProvisionerJobParam(db),
|
||||
)
|
||||
rtr.Get("/", nil)
|
||||
|
||||
r, _ := setup(db)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("provisionerjob", "nothin")
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractProvisionerJobParam(db),
|
||||
)
|
||||
rtr.Get("/", nil)
|
||||
|
||||
r, _ := setup(db)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("provisionerjob", uuid.NewString())
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("ProvisionerJob", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractProvisionerJobParam(db),
|
||||
)
|
||||
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
_ = httpmw.ProvisionerJobParam(r)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
r, job := setup(db)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("provisionerjob", job.ID.String())
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package httpmw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
)
|
||||
|
||||
type workspaceAgentContextKey struct{}
|
||||
|
||||
// WorkspaceAgent returns the workspace agent from the ExtractAgent handler.
|
||||
func WorkspaceAgent(r *http.Request) database.WorkspaceAgent {
|
||||
user, ok := r.Context().Value(workspaceAgentContextKey{}).(database.WorkspaceAgent)
|
||||
if !ok {
|
||||
panic("developer error: agent middleware not provided")
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// ExtractWorkspaceAgent requires authentication using a valid agent token.
|
||||
func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie(AuthCookie)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: fmt.Sprintf("%q cookie must be provided", AuthCookie),
|
||||
})
|
||||
return
|
||||
}
|
||||
token, err := uuid.Parse(cookie.Value)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: fmt.Sprintf("parse token: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
agent, err := db.GetWorkspaceAgentByAuthToken(r.Context(), token)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: "agent token is invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace agent: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), workspaceAgentContextKey{}, agent)
|
||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package httpmw_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/database/databasefake"
|
||||
"github.com/coder/coder/httpmw"
|
||||
)
|
||||
|
||||
func TestWorkspaceAgent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
setup := func(db database.Store) (*http.Request, uuid.UUID) {
|
||||
token := uuid.New()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: httpmw.AuthCookie,
|
||||
Value: token.String(),
|
||||
})
|
||||
return r, token
|
||||
}
|
||||
|
||||
t.Run("None", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractWorkspaceAgent(db),
|
||||
)
|
||||
rtr.Get("/", nil)
|
||||
r, _ := setup(db)
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractWorkspaceAgent(db),
|
||||
)
|
||||
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
_ = httpmw.WorkspaceAgent(r)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
})
|
||||
r, token := setup(db)
|
||||
_, err := db.InsertWorkspaceAgent(context.Background(), database.InsertWorkspaceAgentParams{
|
||||
ID: uuid.New(),
|
||||
AuthToken: token,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package httpmw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
)
|
||||
|
||||
type workspaceBuildParamContextKey struct{}
|
||||
|
||||
// WorkspaceBuildParam returns the workspace build from the ExtractWorkspaceBuildParam handler.
|
||||
func WorkspaceBuildParam(r *http.Request) database.WorkspaceBuild {
|
||||
workspaceBuild, ok := r.Context().Value(workspaceBuildParamContextKey{}).(database.WorkspaceBuild)
|
||||
if !ok {
|
||||
panic("developer error: workspace build param middleware not provided")
|
||||
}
|
||||
return workspaceBuild
|
||||
}
|
||||
|
||||
// ExtractWorkspaceBuildParam grabs workspace build from the "workspacebuild" URL parameter.
|
||||
func ExtractWorkspaceBuildParam(db database.Store) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
workspaceBuildID, parsed := parseUUID(rw, r, "workspacebuild")
|
||||
if !parsed {
|
||||
return
|
||||
}
|
||||
workspaceBuild, err := db.GetWorkspaceBuildByID(r.Context(), workspaceBuildID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("workspace build %q does not exist", workspaceBuildID),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace build: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), workspaceBuildParamContextKey{}, workspaceBuild)
|
||||
// This injects the "workspace" parameter, because it's expected the consumer
|
||||
// will want to use the Workspace middleware to ensure the caller owns the workspace.
|
||||
chi.RouteContext(ctx).URLParams.Add("workspace", workspaceBuild.WorkspaceID.String())
|
||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"github.com/coder/coder/httpmw"
|
||||
)
|
||||
|
||||
func TestWorkspaceHistoryParam(t *testing.T) {
|
||||
func TestWorkspaceBuildParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
setupAuthentication := func(db database.Store) (*http.Request, database.Workspace) {
|
||||
@@ -74,12 +74,7 @@ func TestWorkspaceHistoryParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractUserParam(db),
|
||||
httpmw.ExtractWorkspaceParam(db),
|
||||
httpmw.ExtractWorkspaceHistoryParam(db),
|
||||
)
|
||||
rtr.Use(httpmw.ExtractWorkspaceBuildParam(db))
|
||||
rtr.Get("/", nil)
|
||||
r, _ := setupAuthentication(db)
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -94,16 +89,11 @@ func TestWorkspaceHistoryParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractUserParam(db),
|
||||
httpmw.ExtractWorkspaceParam(db),
|
||||
httpmw.ExtractWorkspaceHistoryParam(db),
|
||||
)
|
||||
rtr.Use(httpmw.ExtractWorkspaceBuildParam(db))
|
||||
rtr.Get("/", nil)
|
||||
|
||||
r, _ := setupAuthentication(db)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("workspacehistory", "nothin")
|
||||
chi.RouteContext(r.Context()).URLParams.Add("workspacebuild", uuid.NewString())
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
@@ -112,60 +102,28 @@ func TestWorkspaceHistoryParam(t *testing.T) {
|
||||
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("WorkspaceHistory", func(t *testing.T) {
|
||||
t.Run("WorkspaceBuild", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractUserParam(db),
|
||||
httpmw.ExtractWorkspaceBuildParam(db),
|
||||
httpmw.ExtractWorkspaceParam(db),
|
||||
httpmw.ExtractWorkspaceHistoryParam(db),
|
||||
)
|
||||
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
_ = httpmw.WorkspaceHistoryParam(r)
|
||||
_ = httpmw.WorkspaceBuildParam(r)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
r, workspace := setupAuthentication(db)
|
||||
workspaceHistory, err := db.InsertWorkspaceHistory(context.Background(), database.InsertWorkspaceHistoryParams{
|
||||
workspaceBuild, err := db.InsertWorkspaceBuild(context.Background(), database.InsertWorkspaceBuildParams{
|
||||
ID: uuid.New(),
|
||||
WorkspaceID: workspace.ID,
|
||||
Name: "moo",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("workspacehistory", workspaceHistory.Name)
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("WorkspaceHistoryLatest", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractUserParam(db),
|
||||
httpmw.ExtractWorkspaceParam(db),
|
||||
httpmw.ExtractWorkspaceHistoryParam(db),
|
||||
)
|
||||
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
_ = httpmw.WorkspaceHistoryParam(r)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
r, workspace := setupAuthentication(db)
|
||||
_, err := db.InsertWorkspaceHistory(context.Background(), database.InsertWorkspaceHistoryParams{
|
||||
ID: uuid.New(),
|
||||
WorkspaceID: workspace.ID,
|
||||
Name: "moo",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("workspacehistory", "latest")
|
||||
chi.RouteContext(r.Context()).URLParams.Add("workspacebuild", workspaceBuild.ID.String())
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
package httpmw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
)
|
||||
|
||||
type workspaceHistoryParamContextKey struct{}
|
||||
|
||||
// WorkspaceHistoryParam returns the workspace history from the ExtractWorkspaceHistoryParam handler.
|
||||
func WorkspaceHistoryParam(r *http.Request) database.WorkspaceHistory {
|
||||
workspaceHistory, ok := r.Context().Value(workspaceHistoryParamContextKey{}).(database.WorkspaceHistory)
|
||||
if !ok {
|
||||
panic("developer error: workspace history param middleware not provided")
|
||||
}
|
||||
return workspaceHistory
|
||||
}
|
||||
|
||||
// ExtractWorkspaceHistoryParam grabs workspace history from the "workspacehistory" URL parameter.
|
||||
func ExtractWorkspaceHistoryParam(db database.Store) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
workspace := WorkspaceParam(r)
|
||||
workspaceHistoryName := chi.URLParam(r, "workspacehistory")
|
||||
if workspaceHistoryName == "" {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: "workspace history name must be provided",
|
||||
})
|
||||
return
|
||||
}
|
||||
var workspaceHistory database.WorkspaceHistory
|
||||
var err error
|
||||
if workspaceHistoryName == "latest" {
|
||||
workspaceHistory, err = db.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: "there is no workspace history",
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
workspaceHistory, err = db.GetWorkspaceHistoryByWorkspaceIDAndName(r.Context(), database.GetWorkspaceHistoryByWorkspaceIDAndNameParams{
|
||||
WorkspaceID: workspace.ID,
|
||||
Name: workspaceHistoryName,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("workspace history %q does not exist", workspaceHistoryName),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace history: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), workspaceHistoryParamContextKey{}, workspaceHistory)
|
||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
+11
-12
@@ -7,8 +7,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
)
|
||||
@@ -28,18 +26,11 @@ func WorkspaceParam(r *http.Request) database.Workspace {
|
||||
func ExtractWorkspaceParam(db database.Store) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
user := UserParam(r)
|
||||
workspaceName := chi.URLParam(r, "workspace")
|
||||
if workspaceName == "" {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: "workspace id must be provided",
|
||||
})
|
||||
workspaceID, parsed := parseUUID(rw, r, "workspace")
|
||||
if !parsed {
|
||||
return
|
||||
}
|
||||
workspace, err := db.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{
|
||||
OwnerID: user.ID,
|
||||
Name: workspaceName,
|
||||
})
|
||||
workspace, err := db.GetWorkspaceByID(r.Context(), workspaceID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("workspace %q does not exist", workspace),
|
||||
@@ -53,6 +44,14 @@ func ExtractWorkspaceParam(db database.Store) func(http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
apiKey := APIKey(r)
|
||||
if apiKey.UserID != workspace.OwnerID {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: "getting non-personal workspaces isn't supported",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), workspaceParamContextKey{}, workspace)
|
||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
|
||||
@@ -66,11 +66,7 @@ func TestWorkspaceParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractUserParam(db),
|
||||
httpmw.ExtractWorkspaceParam(db),
|
||||
)
|
||||
rtr.Use(httpmw.ExtractWorkspaceParam(db))
|
||||
rtr.Get("/", nil)
|
||||
r, _ := setup(db)
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -85,14 +81,10 @@ func TestWorkspaceParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractUserParam(db),
|
||||
httpmw.ExtractWorkspaceParam(db),
|
||||
)
|
||||
rtr.Use(httpmw.ExtractWorkspaceParam(db))
|
||||
rtr.Get("/", nil)
|
||||
r, _ := setup(db)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("workspace", "frog")
|
||||
chi.RouteContext(r.Context()).URLParams.Add("workspace", uuid.NewString())
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
@@ -101,13 +93,37 @@ func TestWorkspaceParam(t *testing.T) {
|
||||
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("NonPersonal", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractWorkspaceParam(db),
|
||||
)
|
||||
rtr.Get("/", nil)
|
||||
r, _ := setup(db)
|
||||
workspace, err := db.InsertWorkspace(context.Background(), database.InsertWorkspaceParams{
|
||||
ID: uuid.New(),
|
||||
OwnerID: "not-me",
|
||||
Name: "hello",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("workspace", workspace.ID.String())
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractAPIKey(db, nil),
|
||||
httpmw.ExtractUserParam(db),
|
||||
httpmw.ExtractWorkspaceParam(db),
|
||||
)
|
||||
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
@@ -121,7 +137,7 @@ func TestWorkspaceParam(t *testing.T) {
|
||||
Name: "hello",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("workspace", workspace.Name)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("workspace", workspace.ID.String())
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package httpmw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
)
|
||||
|
||||
type workspaceResourceParamContextKey struct{}
|
||||
|
||||
// ProvisionerJobParam returns the project from the ExtractProjectParam handler.
|
||||
func WorkspaceResourceParam(r *http.Request) database.WorkspaceResource {
|
||||
resource, ok := r.Context().Value(workspaceResourceParamContextKey{}).(database.WorkspaceResource)
|
||||
if !ok {
|
||||
panic("developer error: workspace resource param middleware not provided")
|
||||
}
|
||||
return resource
|
||||
}
|
||||
|
||||
// ExtractWorkspaceResourceParam grabs a workspace resource from the "provisionerjob" URL parameter.
|
||||
func ExtractWorkspaceResourceParam(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) {
|
||||
resourceUUID, parsed := parseUUID(rw, r, "workspaceresource")
|
||||
if !parsed {
|
||||
return
|
||||
}
|
||||
resource, err := db.GetWorkspaceResourceByID(r.Context(), resourceUUID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: "resource doesn't exist with that id",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner resource: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
job, err := db.GetProvisionerJobByID(r.Context(), resource.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if job.Type != database.ProvisionerJobTypeWorkspaceBuild {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: "Workspace resources can only be fetched for builds.",
|
||||
})
|
||||
return
|
||||
}
|
||||
build, err := db.GetWorkspaceBuildByJobID(r.Context(), job.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace build: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), workspaceResourceParamContextKey{}, resource)
|
||||
ctx = context.WithValue(ctx, workspaceBuildParamContextKey{}, build)
|
||||
chi.RouteContext(ctx).URLParams.Add("workspace", build.WorkspaceID.String())
|
||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package httpmw_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/database/databasefake"
|
||||
"github.com/coder/coder/httpmw"
|
||||
)
|
||||
|
||||
func TestWorkspaceResourceParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
setup := func(db database.Store, jobType database.ProvisionerJobType) (*http.Request, database.WorkspaceResource) {
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
job, err := db.InsertProvisionerJob(context.Background(), database.InsertProvisionerJobParams{
|
||||
ID: uuid.New(),
|
||||
Type: jobType,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
workspaceBuild, err := db.InsertWorkspaceBuild(context.Background(), database.InsertWorkspaceBuildParams{
|
||||
ID: uuid.New(),
|
||||
JobID: job.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resource, err := db.InsertWorkspaceResource(context.Background(), database.InsertWorkspaceResourceParams{
|
||||
ID: uuid.New(),
|
||||
JobID: job.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := chi.NewRouteContext()
|
||||
ctx.URLParams.Add("workspacebuild", workspaceBuild.ID.String())
|
||||
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx))
|
||||
return r, resource
|
||||
}
|
||||
|
||||
t.Run("None", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(httpmw.ExtractWorkspaceResourceParam(db))
|
||||
rtr.Get("/", nil)
|
||||
r, _ := setup(db, database.ProvisionerJobTypeWorkspaceBuild)
|
||||
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.ExtractWorkspaceResourceParam(db),
|
||||
)
|
||||
rtr.Get("/", nil)
|
||||
|
||||
r, _ := setup(db, database.ProvisionerJobTypeWorkspaceBuild)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("workspaceresource", uuid.NewString())
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("FoundBadJobType", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractWorkspaceResourceParam(db),
|
||||
)
|
||||
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
_ = httpmw.WorkspaceResourceParam(r)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
r, job := setup(db, database.ProvisionerJobTypeProjectVersionImport)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("workspaceresource", job.ID.String())
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := databasefake.New()
|
||||
rtr := chi.NewRouter()
|
||||
rtr.Use(
|
||||
httpmw.ExtractWorkspaceResourceParam(db),
|
||||
)
|
||||
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
_ = httpmw.WorkspaceResourceParam(r)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
r, job := setup(db, database.ProvisionerJobTypeWorkspaceBuild)
|
||||
chi.RouteContext(r.Context()).URLParams.Add("workspaceresource", job.ID.String())
|
||||
rw := httptest.NewRecorder()
|
||||
rtr.ServeHTTP(rw, r)
|
||||
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user