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

Skip to content

feat: Add provisionerdaemon to coderd #141

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Feb 3, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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
commit 7a9f7149de88eb7a556d2114311e08022c5cc26a
11 changes: 10 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,14 @@
"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,
}
}
99 changes: 99 additions & 0 deletions coderd/projecthistory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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.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)
})
}
Loading