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:
Kyle Carberry
2022-03-07 11:40:54 -06:00
committed by GitHub
parent 330686f60a
commit bf0ae8f573
115 changed files with 5853 additions and 4657 deletions

View File

@ -7,27 +7,15 @@ import (
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"golang.org/x/xerrors"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
"github.com/coder/coder/httpmw"
)
// ParameterValue represents a set value for the scope.
type ParameterValue database.ParameterValue
// CreateParameterValueRequest is used to create a new parameter value for a scope.
type CreateParameterValueRequest struct {
Name string `json:"name" validate:"required"`
SourceValue string `json:"source_value" validate:"required"`
SourceScheme database.ParameterSourceScheme `json:"source_scheme" validate:"oneof=data,required"`
DestinationScheme database.ParameterDestinationScheme `json:"destination_scheme" validate:"oneof=environment_variable provisioner_variable,required"`
}
// Project is the JSON representation of a Coder project.
// This type matches the database object for now, but is
// abstracted for ease of change later on.
@ -42,48 +30,10 @@ type Project struct {
WorkspaceOwnerCount uint32 `json:"workspace_owner_count"`
}
// CreateProjectRequest enables callers to create a new Project.
type CreateProjectRequest struct {
Name string `json:"name" validate:"username,required"`
// VersionImportJobID is an in-progress or completed job to use as
// an initial version of the project.
//
// This is required on creation to enable a user-flow of validating
// the project works. There is no reason the data-model cannot support
// empty projects, but it doesn't make sense for users.
VersionImportJobID uuid.UUID `json:"import_job_id" validate:"required"`
}
// Lists all projects the authenticated user has access to.
func (api *api) projects(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), apiKey.UserID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organizations: %s", err.Error()),
})
return
}
organizationIDs := make([]string, 0, len(organizations))
for _, organization := range organizations {
organizationIDs = append(organizationIDs, organization.ID)
}
projects, err := api.Database.GetProjectsByOrganizationIDs(r.Context(), organizationIDs)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get projects: %s", err.Error()),
})
return
}
projectIDs := make([]uuid.UUID, 0, len(projects))
for _, project := range projects {
projectIDs = append(projectIDs, project.ID)
}
workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByProjectIDs(r.Context(), projectIDs)
// Returns a single project.
func (api *api) project(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)
workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByProjectIDs(r.Context(), []uuid.UUID{project.ID})
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
@ -93,184 +43,91 @@ func (api *api) projects(rw http.ResponseWriter, r *http.Request) {
})
return
}
count := uint32(0)
if len(workspaceCounts) > 0 {
count = uint32(workspaceCounts[0].Count)
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertProjects(projects, workspaceCounts))
render.JSON(rw, r, convertProject(project, count))
}
// Lists all projects in an organization.
func (api *api) projectsByOrganization(rw http.ResponseWriter, r *http.Request) {
organization := httpmw.OrganizationParam(r)
projects, err := api.Database.GetProjectsByOrganizationIDs(r.Context(), []string{organization.ID})
func (api *api) projectVersionsByProject(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)
versions, err := api.Database.GetProjectVersionsByProjectID(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 projects: %s", err.Error()),
Message: fmt.Sprintf("get project version: %s", err),
})
return
}
projectIDs := make([]uuid.UUID, 0, len(projects))
for _, project := range projects {
projectIDs = append(projectIDs, project.ID)
}
workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByProjectIDs(r.Context(), projectIDs)
if errors.Is(err, sql.ErrNoRows) {
err = nil
jobIDs := make([]uuid.UUID, 0, len(versions))
for _, version := range versions {
jobIDs = append(jobIDs, version.JobID)
}
jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace counts: %s", err.Error()),
Message: fmt.Sprintf("get jobs: %s", err),
})
return
}
jobByID := map[string]database.ProvisionerJob{}
for _, job := range jobs {
jobByID[job.ID.String()] = job
}
apiVersion := make([]ProjectVersion, 0)
for _, version := range versions {
job, exists := jobByID[version.JobID.String()]
if !exists {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("job %q doesn't exist for version %q", version.JobID, version.ID),
})
return
}
apiVersion = append(apiVersion, convertProjectVersion(version, convertProvisionerJob(job)))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertProjects(projects, workspaceCounts))
render.JSON(rw, r, apiVersion)
}
// Create a new project in an organization.
func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Request) {
var createProject CreateProjectRequest
if !httpapi.Read(rw, r, &createProject) {
return
}
organization := httpmw.OrganizationParam(r)
_, err := api.Database.GetProjectByOrganizationAndName(r.Context(), database.GetProjectByOrganizationAndNameParams{
OrganizationID: organization.ID,
Name: createProject.Name,
func (api *api) projectVersionByName(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)
projectVersionName := chi.URLParam(r, "projectversionname")
projectVersion, err := api.Database.GetProjectVersionByProjectIDAndName(r.Context(), database.GetProjectVersionByProjectIDAndNameParams{
ProjectID: uuid.NullUUID{
UUID: project.ID,
Valid: true,
},
Name: projectVersionName,
})
if err == nil {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: fmt.Sprintf("project %q already exists", createProject.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 project by name: %s", err),
})
return
}
importJob, err := api.Database.GetProvisionerJobByID(r.Context(), createProject.VersionImportJobID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: "import job does not exist",
Message: fmt.Sprintf("no project version found by name %q", projectVersionName),
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get import job by id: %s", err),
Message: fmt.Sprintf("get project version by name: %s", err),
})
return
}
var project Project
err = api.Database.InTx(func(db database.Store) error {
projectVersionID := uuid.New()
dbProject, err := db.InsertProject(r.Context(), database.InsertProjectParams{
ID: uuid.New(),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
OrganizationID: organization.ID,
Name: createProject.Name,
Provisioner: importJob.Provisioner,
ActiveVersionID: projectVersionID,
})
if err != nil {
return xerrors.Errorf("insert project: %s", err)
}
_, err = db.InsertProjectVersion(r.Context(), database.InsertProjectVersionParams{
ID: projectVersionID,
ProjectID: dbProject.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Name: namesgenerator.GetRandomName(1),
ImportJobID: importJob.ID,
})
if err != nil {
return xerrors.Errorf("insert project version: %s", err)
}
project = convertProject(dbProject, 0)
return nil
})
job, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, project)
}
// Returns a single project.
func (*api) projectByOrganization(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)
render.Status(r, http.StatusOK)
render.JSON(rw, r, project)
}
// Creates parameters for a project.
// This should validate the calling user has permissions!
func (api *api) postParametersByProject(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)
var createRequest CreateParameterValueRequest
if !httpapi.Read(rw, r, &createRequest) {
return
}
parameterValue, err := api.Database.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
ID: uuid.New(),
Name: createRequest.Name,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Scope: database.ParameterScopeProject,
ScopeID: project.ID.String(),
SourceScheme: createRequest.SourceScheme,
SourceValue: createRequest.SourceValue,
DestinationScheme: createRequest.DestinationScheme,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert parameter value: %s", err),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, parameterValue)
}
// Lists parameters for a project.
func (api *api) parametersByProject(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)
parameterValues, err := api.Database.GetParameterValuesByScope(r.Context(), database.GetParameterValuesByScopeParams{
Scope: database.ParameterScopeProject,
ScopeID: project.ID.String(),
})
if errors.Is(err, sql.ErrNoRows) {
err = nil
parameterValues = []database.ParameterValue{}
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get parameter values: %s", err),
})
return
}
apiParameterValues := make([]ParameterValue, 0, len(parameterValues))
for _, parameterValue := range parameterValues {
apiParameterValues = append(apiParameterValues, convertParameterValue(parameterValue))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiParameterValues)
render.JSON(rw, r, convertProjectVersion(projectVersion, convertProvisionerJob(job)))
}
func convertProjects(projects []database.Project, workspaceCounts []database.GetWorkspaceOwnerCountsByProjectIDsRow) []Project {
@ -304,8 +161,3 @@ func convertProject(project database.Project, workspaceOwnerCount uint32) Projec
WorkspaceOwnerCount: workspaceOwnerCount,
}
}
func convertParameterValue(parameterValue database.ParameterValue) ParameterValue {
parameterValue.SourceValue = ""
return ParameterValue(parameterValue)
}