feat: Validate swagger definitions (#5694)

* docs: audit, deploymentconfig, files, parameters

* Swagger comments in workspacebuilds.go

* structs in workspacebuilds.go

* workspaceagents: instance identity

* workspaceagents.go in progress

* workspaceagents.go in progress

* Agents

* workspacebuilds.go

* /workspaces

* templates.go, templateversions.go

* templateversion.go in progress

* cancel

* templateversions

* wip

* Merge

* x-apidocgen

* NullTime hack not needed anymore

* Fix: x-apidocgen

* Members

* Fixes

* Fix

* WIP

* WIP

* Users

* Logout

* User profile

* Status suspend activate

* User roles

* User tokens

* Keys

* SSH key

* All

* Typo

* Fix

* Entitlements

* Groups

* SCIM

* Fix

* Fix

* Clean templates

* Sort API pages

* Fix: HashedSecret

* WIP

* WIP

* WIP

* Fix: cover workspaceagents

* Assert: consistent ID and summary

* Assert: success or failure defined

* Fix: parallel

* Refactor

* Support enterprise

* Go comment goes to top

* Security

* assertPathParametersDefined

* assertUniqueRoutes

* assertRequestBody

* More fixes

* Fix: exceptions

* Fix field format

* Address PR comments

* Refactor
This commit is contained in:
Marcin Tojek
2023-01-13 12:27:21 +01:00
committed by GitHub
parent dcab87358e
commit deebfcbd53
39 changed files with 2442 additions and 705 deletions
+622 -264
View File
File diff suppressed because it is too large Load Diff
+542 -222
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -29,6 +29,7 @@ import (
// @Summary Create token API key
// @ID create-token-api-key
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Users
// @Param user path string true "User ID, name, or me"
@@ -209,9 +210,8 @@ func (api *API) tokens(rw http.ResponseWriter, r *http.Request) {
}
// @Summary Delete API key
// @ID delete-user-tokens
// @ID delete-api-key
// @Security CoderSessionToken
// @Produce json
// @Tags Users
// @Param user path string true "User ID, name, or me"
// @Param keyid path string true "Key ID" format(uuid)
+1 -1
View File
@@ -89,7 +89,7 @@ func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) {
}
// @Summary Generate fake audit log
// @ID generate-fake-audit-logs
// @ID generate-fake-audit-log
// @Security CoderSessionToken
// @Accept json
// @Tags Audit
+66
View File
@@ -0,0 +1,66 @@
package coderdtest_test
import (
"go/ast"
"go/parser"
"go/token"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
)
func TestEndpointsDocumented(t *testing.T) {
t.Parallel()
swaggerComments, err := coderdtest.ParseSwaggerComments("..")
require.NoError(t, err, "can't parse swagger comments")
_, _, api := coderdtest.NewWithAPI(t, nil)
coderdtest.VerifySwaggerDefinitions(t, api.APIHandler, swaggerComments)
}
func TestSDKFieldsFormatted(t *testing.T) {
t.Parallel()
fileSet := token.NewFileSet()
nodes, err := parser.ParseDir(fileSet, "../../codersdk", nil, parser.ParseComments)
require.NoError(t, err, "parser.ParseDir failed")
for _, node := range nodes {
ast.Inspect(node, func(n ast.Node) bool {
typeSpec, ok := n.(*ast.TypeSpec)
if !ok {
return true
}
structureName := typeSpec.Name
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
return true // not a structure
}
for _, field := range structType.Fields.List {
selectorExpr, ok := field.Type.(*ast.SelectorExpr)
if !ok {
continue // rather a basic, or primitive
}
if field.Tag == nil || !strings.Contains(field.Tag.Value, `json:"`) {
continue // not a JSON property
}
switch selectorExpr.Sel.Name {
case "UUID":
assert.Contains(t, field.Tag.Value, `format:"uuid"`, `Swagger formatting requires to annotate the field with - format:"uuid". Location: %s/%s`, structureName, field.Names)
case "Time":
assert.Contains(t, field.Tag.Value, `format:"date-time"`, `Swagger formatting requires to annotate the field with - format:"date-time". Location: %s/%s`, structureName, field.Names)
}
}
return true
})
}
}
+318
View File
@@ -0,0 +1,318 @@
package coderdtest
import (
"go/ast"
"go/parser"
"go/token"
"net/http"
"regexp"
"strings"
"testing"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
)
type SwaggerComment struct {
summary string
id string
security string
tags string
accept string
produce string
method string
router string
successes []response
failures []response
parameters []parameter
raw []*ast.Comment
}
type parameter struct {
name string
kind string
}
type response struct {
status string
kind string // {object} or {array}
model string
}
func ParseSwaggerComments(dirs ...string) ([]SwaggerComment, error) {
fileSet := token.NewFileSet()
var swaggerComments []SwaggerComment
for _, dir := range dirs {
nodes, err := parser.ParseDir(fileSet, dir, nil, parser.ParseComments)
if err != nil {
return nil, xerrors.Errorf(`parser.ParseDir failed for "%s": %w`, dir, err)
}
for _, node := range nodes {
ast.Inspect(node, func(n ast.Node) bool {
commentGroup, ok := n.(*ast.CommentGroup)
if !ok {
return true
}
var isSwaggerComment bool
for _, line := range commentGroup.List {
text := strings.TrimSpace(line.Text)
if strings.HasPrefix(text, "//") && strings.Contains(text, "@Router") {
isSwaggerComment = true
break
}
}
if isSwaggerComment {
swaggerComments = append(swaggerComments, parseSwaggerComment(commentGroup))
}
return true
})
}
}
return swaggerComments, nil
}
func parseSwaggerComment(commentGroup *ast.CommentGroup) SwaggerComment {
c := SwaggerComment{
raw: commentGroup.List,
parameters: []parameter{},
successes: []response{},
failures: []response{},
}
for _, line := range commentGroup.List {
// @<annotationName> [args...]
splitN := strings.SplitN(strings.TrimSpace(line.Text), " ", 3)
if len(splitN) < 2 {
continue // comment prefix without any content
}
if !strings.HasPrefix(splitN[1], "@") {
continue // not a swagger annotation
}
annotationName := splitN[1]
annotationArgs := splitN[2]
args := strings.Split(splitN[2], " ")
switch annotationName {
case "@Router":
c.router = args[0]
c.method = args[1][1 : len(args[1])-1]
case "@Success", "@Failure":
var r response
if len(args) > 0 {
r.status = args[0]
}
if len(args) > 1 {
r.kind = args[1]
}
if len(args) > 2 {
r.model = args[2]
}
if annotationName == "@Success" {
c.successes = append(c.successes, r)
} else if annotationName == "@Failure" {
c.failures = append(c.failures, r)
}
case "@Param":
p := parameter{
name: args[0],
kind: args[1],
}
c.parameters = append(c.parameters, p)
case "@Summary":
c.summary = annotationArgs
case "@ID":
c.id = annotationArgs
case "@Tags":
c.tags = annotationArgs
case "@Security":
c.security = annotationArgs
case "@Accept":
c.accept = annotationArgs
case "@Produce":
c.produce = annotationArgs
}
}
return c
}
func VerifySwaggerDefinitions(t *testing.T, router chi.Router, swaggerComments []SwaggerComment) {
assertUniqueRoutes(t, swaggerComments)
err := chi.Walk(router, func(method, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
method = strings.ToLower(method)
if route != "/" && strings.HasSuffix(route, "/") {
route = route[:len(route)-1]
}
t.Run(method+" "+route, func(t *testing.T) {
t.Parallel()
c := findSwaggerCommentByMethodAndRoute(swaggerComments, method, route)
assert.NotNil(t, c, "Missing @Router annotation")
if c == nil {
return // do not fail next assertion for this route
}
assertConsistencyBetweenRouteIDAndSummary(t, *c)
assertSuccessOrFailureDefined(t, *c)
assertRequiredAnnotations(t, *c)
assertGoCommentFirst(t, *c)
assertPathParametersDefined(t, *c)
assertSecurityDefined(t, *c)
assertAccept(t, *c)
assertProduce(t, *c)
})
return nil
})
require.NoError(t, err, "chi.Walk should not fail")
}
func assertUniqueRoutes(t *testing.T, comments []SwaggerComment) {
m := map[string]struct{}{}
for _, c := range comments {
key := c.method + " " + c.router
_, alreadyDefined := m[key]
assert.False(t, alreadyDefined, "defined route must be unique (method: %s, route: %s)", c.method, c.router)
if !alreadyDefined {
m[key] = struct{}{}
}
}
}
func findSwaggerCommentByMethodAndRoute(comments []SwaggerComment, method, route string) *SwaggerComment {
for _, c := range comments {
if c.method == method && c.router == route {
return &c
}
}
return nil
}
var nonAlphanumericRegex = regexp.MustCompile(`[^a-zA-Z0-9-]+`)
func assertConsistencyBetweenRouteIDAndSummary(t *testing.T, comment SwaggerComment) {
exp := strings.ToLower(comment.summary)
exp = strings.ReplaceAll(exp, " ", "-")
exp = nonAlphanumericRegex.ReplaceAllString(exp, "")
assert.Equal(t, exp, comment.id, "Router ID must match summary")
}
func assertSuccessOrFailureDefined(t *testing.T, comment SwaggerComment) {
assert.True(t, len(comment.successes) > 0 || len(comment.failures) > 0, "At least one @Success or @Failure annotation must be defined")
}
func assertRequiredAnnotations(t *testing.T, comment SwaggerComment) {
assert.NotEmpty(t, comment.id, "@ID must be defined")
assert.NotEmpty(t, comment.summary, "@Summary must be defined")
assert.NotEmpty(t, comment.tags, "@Tags must be defined")
}
func assertGoCommentFirst(t *testing.T, comment SwaggerComment) {
var inSwaggerBlock bool
for _, line := range comment.raw {
text := strings.TrimSpace(line.Text)
if inSwaggerBlock {
if !strings.HasPrefix(text, "// @") {
assert.Fail(t, "Go function comment must be placed before swagger comments")
return
}
}
if strings.HasPrefix(text, "// @Summary") {
inSwaggerBlock = true
}
}
}
var urlParameterRegexp = regexp.MustCompile(`{[^{}]*}`)
func assertPathParametersDefined(t *testing.T, comment SwaggerComment) {
matches := urlParameterRegexp.FindAllString(comment.router, -1)
if matches == nil {
return // router does not require any parameters
}
for _, m := range matches {
var matched bool
for _, p := range comment.parameters {
if p.kind == "path" && "{"+p.name+"}" == m {
matched = true
break
}
}
if !matched {
assert.Failf(t, "Missing @Param annotation", "Path parameter: %s", m)
}
}
}
func assertSecurityDefined(t *testing.T, comment SwaggerComment) {
if comment.router == "/updatecheck" ||
comment.router == "/buildinfo" ||
comment.router == "/" {
return // endpoints do not require authorization
}
assert.Equal(t, "CoderSessionToken", comment.security, "@Security must be equal CoderSessionToken")
}
func assertAccept(t *testing.T, comment SwaggerComment) {
var hasRequestBody bool
for _, c := range comment.parameters {
if c.name == "request" && c.kind == "body" ||
c.name == "file" && c.kind == "formData" {
hasRequestBody = true
break
}
}
var hasAccept bool
if comment.accept != "" {
hasAccept = true
}
if comment.method == "get" {
assert.Empty(t, comment.accept, "GET route does not require the @Accept annotation")
assert.False(t, hasRequestBody, "GET route does not require the request body")
} else {
assert.False(t, hasRequestBody && !hasAccept, "Route with the request body requires the @Accept annotation")
assert.False(t, !hasRequestBody && hasAccept, "Route with @Accept annotation requires the request body or file formData parameter")
}
}
func assertProduce(t *testing.T, comment SwaggerComment) {
var hasResponseModel bool
for _, r := range comment.successes {
if r.model != "" {
hasResponseModel = true
break
}
}
if hasResponseModel {
assert.True(t, comment.produce != "", "Route must have @Produce annotation as it responds with a model structure")
} else {
if (comment.router == "/workspaceagents/me/app-health" && comment.method == "post") ||
(comment.router == "/workspaceagents/me/version" && comment.method == "post") ||
(comment.router == "/licenses/{id}" && comment.method == "delete") {
return // Exception: HTTP 200 is returned without response entity
}
assert.True(t, comment.produce == "", "Response model is undefined, so we can't predict the content type", comment)
}
}
-1
View File
@@ -20,7 +20,6 @@ type cspViolation struct {
// @ID report-csp-violations
// @Security CoderSessionToken
// @Accept json
// @Produce text/plain
// @Tags General
// @Param request body cspViolation true "Violation report"
// @Success 200
+1 -1
View File
@@ -25,7 +25,7 @@ const (
// @Summary Upload file
// @Description Swagger notice: Swagger 2.0 doesn't support file upload with a `content-type` different than `application/x-www-form-urlencoded`.
// @ID update-file
// @ID upload-file
// @Security CoderSessionToken
// @Produce json
// @Accept application/x-tar
-1
View File
@@ -119,7 +119,6 @@ func (api *API) gitSSHKey(rw http.ResponseWriter, r *http.Request) {
// @Summary Get workspace agent Git SSH key
// @ID get-workspace-agent-git-ssh-key
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Agents
// @Success 200 {object} codersdk.AgentGitSSHKey
+1
View File
@@ -40,6 +40,7 @@ func (api *API) organization(rw http.ResponseWriter, r *http.Request) {
// @Summary Create organization
// @ID create-organization
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Organizations
// @Param request body codersdk.CreateOrganizationRequest true "Create organization request"
+14 -12
View File
@@ -23,15 +23,16 @@ import (
"github.com/coder/coder/examples"
)
// Returns a single template.
//
// @Summary Get template metadata by ID
// @ID get-template-metadata-by-id
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param id path string true "Template ID" format(uuid)
// @Param template path string true "Template ID" format(uuid)
// @Success 200 {object} codersdk.Template
// @Router /templates/{id} [get]
// Returns a single template.
// @Router /templates/{template} [get]
func (api *API) template(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
template := httpmw.TemplateParam(r)
@@ -75,9 +76,9 @@ func (api *API) template(rw http.ResponseWriter, r *http.Request) {
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param id path string true "Template ID" format(uuid)
// @Param template path string true "Template ID" format(uuid)
// @Success 200 {object} codersdk.Response
// @Router /templates/{id} [delete]
// @Router /templates/{template} [delete]
func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
@@ -131,6 +132,9 @@ func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
})
}
// Create a new template in an organization.
// Returns a single template.
//
// @Summary Create template by organization
// @ID create-template-by-organization
// @Security CoderSessionToken
@@ -141,8 +145,6 @@ func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
// @Param organization path string true "Organization ID"
// @Success 200 {object} codersdk.Template
// @Router /organizations/{organization}/templates [post]
// Returns a single template.
// Create a new template in an organization.
func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
@@ -461,13 +463,13 @@ func (api *API) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Re
}
// @Summary Update template metadata by ID
// @ID update-template-metadata
// @ID update-template-metadata-by-id
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param id path string true "Template ID" format(uuid)
// @Param template path string true "Template ID" format(uuid)
// @Success 200 {object} codersdk.Template
// @Router /templates/{id} [patch]
// @Router /templates/{template} [patch]
func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
@@ -593,9 +595,9 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param id path string true "Template ID" format(uuid)
// @Param template path string true "Template ID" format(uuid)
// @Success 200 {object} codersdk.TemplateDAUsResponse
// @Router /templates/{id}/daus [get]
// @Router /templates/{template}/daus [get]
func (api *API) templateDAUs(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
template := httpmw.TemplateParam(r)
+29 -29
View File
@@ -353,13 +353,12 @@ func (api *API) postTemplateVersionDryRun(rw http.ResponseWriter, r *http.Reques
// @Summary Get template version dry-run by job ID
// @ID get-template-version-dry-run-by-job-id
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Templates
// @Param templateversion path string true "Template version ID" format(uuid)
// @Param jobid path string true "Job ID" format(uuid)
// @Param jobID path string true "Job ID" format(uuid)
// @Success 200 {object} codersdk.ProvisionerJob
// @Router /templateversions/{templateversion}/dry-run/{jobid} [get]
// @Router /templateversions/{templateversion}/dry-run/{jobID} [get]
func (api *API) templateVersionDryRun(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
job, ok := api.fetchTemplateVersionDryRunJob(rw, r)
@@ -376,9 +375,9 @@ func (api *API) templateVersionDryRun(rw http.ResponseWriter, r *http.Request) {
// @Produce json
// @Tags Templates
// @Param templateversion path string true "Template version ID" format(uuid)
// @Param jobid path string true "Job ID" format(uuid)
// @Param jobID path string true "Job ID" format(uuid)
// @Success 200 {array} codersdk.WorkspaceResource
// @Router /templateversions/{templateversion}/dry-run/{jobid}/resources [get]
// @Router /templateversions/{templateversion}/dry-run/{jobID}/resources [get]
func (api *API) templateVersionDryRunResources(rw http.ResponseWriter, r *http.Request) {
job, ok := api.fetchTemplateVersionDryRunJob(rw, r)
if !ok {
@@ -394,12 +393,12 @@ func (api *API) templateVersionDryRunResources(rw http.ResponseWriter, r *http.R
// @Produce json
// @Tags Templates
// @Param templateversion path string true "Template version ID" format(uuid)
// @Param jobid path string true "Job ID" format(uuid)
// @Param jobID path string true "Job ID" format(uuid)
// @Param before query int false "Before Unix timestamp"
// @Param after query int false "After Unix timestamp"
// @Param follow query bool false "Follow log stream"
// @Success 200 {array} codersdk.ProvisionerJobLog
// @Router /templateversions/{templateversion}/dry-run/{jobid}/logs [get]
// @Router /templateversions/{templateversion}/dry-run/{jobID}/logs [get]
func (api *API) templateVersionDryRunLogs(rw http.ResponseWriter, r *http.Request) {
job, ok := api.fetchTemplateVersionDryRunJob(rw, r)
if !ok {
@@ -414,9 +413,10 @@ func (api *API) templateVersionDryRunLogs(rw http.ResponseWriter, r *http.Reques
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param jobID path string true "Job ID" format(uuid)
// @Param templateversion path string true "Template version ID" format(uuid)
// @Success 200 {object} codersdk.Response
// @Router /templateversions/{templateversion}/dry-run/{jobid}/cancel [patch]
// @Router /templateversions/{templateversion}/dry-run/{jobID}/cancel [patch]
func (api *API) patchTemplateVersionDryRunCancel(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
templateVersion := httpmw.TemplateVersionParam(r)
@@ -536,16 +536,16 @@ func (api *API) fetchTemplateVersionDryRunJob(rw http.ResponseWriter, r *http.Re
}
// @Summary List template versions by template ID
// @ID list-template-versions-by-template-ID
// @ID list-template-versions-by-template-id
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param id path string true "Template ID" format(uuid)
// @Param template path string true "Template ID" format(uuid)
// @Param after_id query string false "After ID" format(uuid)
// @Param limit query int false "Page limit"
// @Param offset query int false "Page offset"
// @Success 200 {array} codersdk.TemplateVersion
// @Router /templates/{id}/versions [get]
// @Router /templates/{template}/versions [get]
func (api *API) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
template := httpmw.TemplateParam(r)
@@ -648,10 +648,10 @@ func (api *API) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Reque
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param id path string true "Template ID" format(uuid)
// @Param template path string true "Template ID" format(uuid)
// @Param templateversionname path string true "Template version name"
// @Success 200 {array} codersdk.TemplateVersion
// @Router /templates/{id}/versions/{templateversionname} [get]
// @Router /templates/{template}/versions/{templateversionname} [get]
func (api *API) templateVersionByName(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
template := httpmw.TemplateParam(r)
@@ -828,15 +828,15 @@ func (api *API) previousTemplateVersionByOrganizationAndName(rw http.ResponseWri
}
// @Summary Update active template version by template ID
// @ID update-active-template-version-by-template-ID
// @ID update-active-template-version-by-template-id
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Templates
// @Param request body codersdk.UpdateActiveTemplateVersion true "Modified template version"
// @Param id path string true "Template ID" format(uuid)
// @Param template path string true "Template ID" format(uuid)
// @Success 200 {object} codersdk.Response
// @Router /templates/{id}/versions [patch]
// @Router /templates/{template}/versions [patch]
func (api *API) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
@@ -911,6 +911,8 @@ func (api *API) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Reque
})
}
// postTemplateVersionsByOrganization creates a new version of a template. An import job is queued to parse the storage method provided.
//
// @Summary Create template version by organization
// @ID create-template-version-by-organization
// @Security CoderSessionToken
@@ -921,8 +923,6 @@ func (api *API) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Reque
// @Param request body codersdk.CreateTemplateVersionDryRunRequest true "Create template version request"
// @Success 201 {object} codersdk.TemplateVersion
// @Router /organizations/{organization}/templateversions [post]
//
// postTemplateVersionsByOrganization creates a new version of a template. An import job is queued to parse the storage method provided.
func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
@@ -1207,6 +1207,12 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
httpapi.Write(ctx, rw, http.StatusCreated, convertTemplateVersion(templateVersion, convertProvisionerJob(provisionerJob), user))
}
// templateVersionResources returns the workspace agent resources associated
// with a template version. A template can specify more than one resource to be
// provisioned, each resource can have an agent that dials back to coderd. The
// agents returned are informative of the template version, and do not return
// agents associated with any particular workspace.
//
// @Summary Get resources by template version
// @ID get-resources-by-template-version
// @Security CoderSessionToken
@@ -1215,12 +1221,6 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
// @Param templateversion path string true "Template version ID" format(uuid)
// @Success 200 {array} codersdk.WorkspaceResource
// @Router /templateversions/{templateversion}/resources [get]
//
// templateVersionResources returns the workspace agent resources associated
// with a template version. A template can specify more than one resource to be
// provisioned, each resource can have an agent that dials back to coderd. The
// agents returned are informative of the template version, and do not return
// agents associated with any particular workspace.
func (api *API) templateVersionResources(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
@@ -1244,6 +1244,11 @@ func (api *API) templateVersionResources(rw http.ResponseWriter, r *http.Request
api.provisionerJobResources(rw, r, job)
}
// templateVersionLogs returns the logs returned by the provisioner for the given
// template version. These logs are only associated with the template version,
// and not any build logs for a workspace.
// Eg: Logs returned from 'terraform plan' when uploading a new terraform file.
//
// @Summary Get logs by template version
// @ID get-logs-by-template-version
// @Security CoderSessionToken
@@ -1255,11 +1260,6 @@ func (api *API) templateVersionResources(rw http.ResponseWriter, r *http.Request
// @Param follow query bool false "Follow log stream"
// @Success 200 {array} codersdk.ProvisionerJobLog
// @Router /templateversions/{templateversion}/logs [get]
//
// templateVersionLogs returns the logs returned by the provisioner for the given
// template version. These logs are only associated with the template version,
// and not any build logs for a workspace.
// Eg: Logs returned from 'terraform plan' when uploading a new terraform file.
func (api *API) templateVersionLogs(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
+2 -4
View File
@@ -59,9 +59,8 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) {
}
// @Summary OAuth 2.0 GitHub Callback
// @ID oauth2-github-callback
// @ID oauth-20-github-callback
// @Security CoderSessionToken
// @Produce json
// @Tags Users
// @Success 307
// @Router /users/oauth2/github/callback [get]
@@ -218,9 +217,8 @@ type OIDCConfig struct {
}
// @Summary OpenID Connect Callback
// @ID oidc-callback
// @ID openid-connect-callback
// @Security CoderSessionToken
// @Produce json
// @Tags Users
// @Success 307
// @Router /users/oidc/callback [get]
+4 -2
View File
@@ -435,6 +435,7 @@ func (api *API) userByName(rw http.ResponseWriter, r *http.Request) {
// @Summary Update user profile
// @ID update-user-profile
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Users
// @Param user path string true "User ID, name, or me"
@@ -617,7 +618,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW
// @Summary Update user password
// @ID update-user-password
// @Security CoderSessionToken
// @Produce json
// @Accept json
// @Tags Users
// @Param user path string true "User ID, name, or me"
// @Param request body codersdk.UpdateUserPasswordRequest true "Update password request"
@@ -908,7 +909,7 @@ func (api *API) updateSiteUserRoles(ctx context.Context, args database.UpdateUse
// Returns organizations the parameterized user has access to.
//
// @Summary Get organizations by user
// @ID get-organizations-by-users
// @ID get-organizations-by-user
// @Security CoderSessionToken
// @Produce json
// @Tags Users
@@ -990,6 +991,7 @@ func (api *API) organizationByUserAndName(rw http.ResponseWriter, r *http.Reques
// @Summary Log in user
// @ID log-in-user
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Authorization
// @Param request body codersdk.LoginWithPasswordRequest true "Login request"
+49 -9
View File
@@ -35,6 +35,14 @@ import (
"github.com/coder/coder/tailnet"
)
// @Summary Get workspace agent by ID
// @ID get-workspace-agent-by-id
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
// @Success 200 {object} codersdk.WorkspaceAgent
// @Router /workspaceagents/{workspaceagent} [get]
func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceAgent := httpmw.WorkspaceAgentParam(r)
@@ -66,7 +74,6 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
// @Summary Get authorized workspace agent metadata
// @ID get-authorized-workspace-agent-metadata
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Agents
// @Success 200 {object} codersdk.WorkspaceAgentMetadata
@@ -147,9 +154,10 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request)
}
// @Summary Submit workspace agent version
// @ID submit-workspace-workspace-agent-version
// @ID submit-workspace-agent-version
// @Security CoderSessionToken
// @Produce application/json
// @Accept json
// @Produce json
// @Tags Agents
// @Param request body codersdk.PostWorkspaceAgentVersionRequest true "Version request"
// @Success 200
@@ -198,6 +206,14 @@ func (api *API) postWorkspaceAgentVersion(rw http.ResponseWriter, r *http.Reques
// workspaceAgentPTY spawns a PTY and pipes it over a WebSocket.
// This is used for the web terminal.
//
// @Summary Open PTY to workspace agent
// @ID open-pty-to-workspace-agent
// @Security CoderSessionToken
// @Tags Agents
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
// @Success 101
// @Router /workspaceagents/{workspaceagent}/pty [get]
func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -276,6 +292,14 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) {
agent.Bicopy(ctx, wsNetConn, ptNetConn)
}
// @Summary Get listening ports for workspace agent
// @ID get-listening-ports-for-workspace-agent
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
// @Success 200 {object} codersdk.ListeningPortsResponse
// @Router /workspaceagents/{workspaceagent}/listening-ports [get]
func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspace := httpmw.WorkspaceParam(r)
@@ -443,6 +467,14 @@ func (api *API) dialWorkspaceAgentTailnet(r *http.Request, agentID uuid.UUID) (*
}, nil
}
// @Summary Get connection info for workspace agent
// @ID get-connection-info-for-workspace-agent
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
// @Success 200 {object} codersdk.WorkspaceAgentConnectionInfo
// @Router /workspaceagents/{workspaceagent}/connection [get]
func (api *API) workspaceAgentConnection(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspace := httpmw.WorkspaceParam(r)
@@ -458,9 +490,8 @@ func (api *API) workspaceAgentConnection(rw http.ResponseWriter, r *http.Request
// @Summary Coordinate workspace agent via Tailnet
// @Description It accepts a WebSocket connection to an agent that listens to
// @Description incoming connections and publishes node updates.
// @ID get-workspace-agent-git-ssh-key-via-tailnet
// @ID coordinate-workspace-agent-via-tailnet
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
// @Success 101
// @Router /workspaceagents/me/coordinate [get]
@@ -622,6 +653,14 @@ func (api *API) workspaceAgentCoordinate(rw http.ResponseWriter, r *http.Request
// workspaceAgentClientCoordinate accepts a WebSocket that reads node network updates.
// After accept a PubSub starts listening for new connection node updates
// which are written to the WebSocket.
//
// @Summary Coordinate workspace agent
// @ID coordinate-workspace-agent
// @Security CoderSessionToken
// @Tags Agents
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
// @Success 101
// @Router /workspaceagents/{workspaceagent}/coordinate [get]
func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -784,8 +823,9 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin
}
// @Summary Submit workspace agent stats
// @ID submit-workspace-workspace-agent-stats
// @ID submit-workspace-agent-stats
// @Security CoderSessionToken
// @Accept json
// @Produce application/json
// @Tags Agents
// @Param request body codersdk.AgentStats true "Stats request"
@@ -860,9 +900,10 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
})
}
// @Summary Submit workspace application health
// @ID submit-workspace-workspace-agent-health
// @Summary Submit workspace agent application health
// @ID submit-workspace-agent-application-health
// @Security CoderSessionToken
// @Accept json
// @Produce application/json
// @Tags Agents
// @Param request body codersdk.PostWorkspaceAppHealthsRequest true "Application health request"
@@ -989,7 +1030,6 @@ func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request)
// @Summary Get workspace agent Git auth
// @ID get-workspace-agent-git-auth
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Agents
// @Param url query string true "Git URL" format(uri)
+8 -8
View File
@@ -66,7 +66,7 @@ var nonCanonicalHeaders = map[string]string{
}
// @Summary Get applications host
// @ID get-app-host
// @ID get-applications-host
// @Security CoderSessionToken
// @Produce json
// @Tags Applications
@@ -614,6 +614,13 @@ func (api *API) setWorkspaceAppCookie(rw http.ResponseWriter, r *http.Request, t
return true
}
// workspaceApplicationAuth is an endpoint on the main router that handles
// redirects from the subdomain handler.
//
// This endpoint is under /api so we don't return the friendly error page here.
// Any errors on this endpoint should be errors that are unlikely to happen
// in production unless the user messes with the URL.
//
// @Summary Redirect to URI with encrypted API key
// @ID redirect-to-uri-with-encrypted-api-key
// @Security CoderSessionToken
@@ -621,13 +628,6 @@ func (api *API) setWorkspaceAppCookie(rw http.ResponseWriter, r *http.Request, t
// @Param redirect_uri query string false "Redirect destination"
// @Success 307
// @Router /applications/auth-redirect [get]
//
// workspaceApplicationAuth is an endpoint on the main router that handles
// redirects from the subdomain handler.
//
// This endpoint is under /api so we don't return the friendly error page here.
// Any errors on this endpoint should be errors that are unlikely to happen
// in production unless the user messes with the URL.
func (api *API) workspaceApplicationAuth(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if api.AppHostname == "" {
+5 -5
View File
@@ -77,13 +77,13 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
// @Param id path string true "Workspace ID" format(uuid)
// @Param workspace path string true "Workspace ID" format(uuid)
// @Param after_id query string false "After ID" format(uuid)
// @Param limit query int false "Page limit"
// @Param offset query int false "Page offset"
// @Param since query string false "Since timestamp" format(date-time)
// @Success 200 {array} codersdk.WorkspaceBuild
// @Router /workspaces/{id}/builds [get]
// @Router /workspaces/{workspace}/builds [get]
func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspace := httpmw.WorkspaceParam(r)
@@ -290,13 +290,13 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
// @Summary Create workspace build
// @ID create-workspace-build
// @Security CoderSessionToken
// @Accepts json
// @Accept json
// @Produce json
// @Tags Builds
// @Param id path string true "Workspace ID" format(uuid)
// @Param workspace path string true "Workspace ID" format(uuid)
// @Param request body codersdk.CreateWorkspaceBuildRequest true "Create workspace build request"
// @Success 200 {object} codersdk.WorkspaceBuild
// @Router /workspaces/{id}/builds [post]
// @Router /workspaces/{workspace}/builds [post]
func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
+11 -8
View File
@@ -23,6 +23,7 @@ import (
// @Summary Authenticate agent on Azure instance
// @ID authenticate-agent-on-azure-instance
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Agents
// @Param request body codersdk.AzureInstanceIdentityToken true "Instance identity token"
@@ -45,18 +46,19 @@ func (api *API) postWorkspaceAuthAzureInstanceIdentity(rw http.ResponseWriter, r
api.handleAuthInstanceID(rw, r, instanceID)
}
// AWS supports instance identity verification:
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
// Using this, we can exchange a signed instance payload for an agent token.
//
// @Summary Authenticate agent on AWS instance
// @ID authenticate-agent-on-aws-instance
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Agents
// @Param request body codersdk.AWSInstanceIdentityToken true "Instance identity token"
// @Success 200 {object} codersdk.WorkspaceAgentAuthenticateResponse
// @Router /workspaceagents/aws-instance-identity [post]
//
// AWS supports instance identity verification:
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
// Using this, we can exchange a signed instance payload for an agent token.
func (api *API) postWorkspaceAuthAWSInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req codersdk.AWSInstanceIdentityToken
@@ -74,18 +76,19 @@ func (api *API) postWorkspaceAuthAWSInstanceIdentity(rw http.ResponseWriter, r *
api.handleAuthInstanceID(rw, r, identity.InstanceID)
}
// Google Compute Engine supports instance identity verification:
// https://cloud.google.com/compute/docs/instances/verifying-instance-identity
// Using this, we can exchange a signed instance payload for an agent token.
//
// @Summary Authenticate agent on Google Cloud instance
// @ID authenticate-agent-on-google-cloud-instance
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Agents
// @Param request body codersdk.GoogleInstanceIdentityToken true "Instance identity token"
// @Success 200 {object} codersdk.WorkspaceAgentAuthenticateResponse
// @Router /workspaceagents/google-instance-identity [post]
//
// Google Compute Engine supports instance identity verification:
// https://cloud.google.com/compute/docs/instances/verifying-instance-identity
// Using this, we can exchange a signed instance payload for an agent token.
func (api *API) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req codersdk.GoogleInstanceIdentityToken
+9 -12
View File
@@ -48,10 +48,10 @@ var (
// @Security CoderSessionToken
// @Produce json
// @Tags Workspaces
// @Param id path string true "Workspace ID" format(uuid)
// @Param workspace path string true "Workspace ID" format(uuid)
// @Param include_deleted query bool false "Return data instead of HTTP 404 if the workspace is deleted"
// @Success 200 {object} codersdk.Workspace
// @Router /workspaces/{id} [get]
// @Router /workspaces/{workspace} [get]
func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspace := httpmw.WorkspaceParam(r)
@@ -101,8 +101,11 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
))
}
// workspaces returns all workspaces a user can read.
// Optional filters with query params
//
// @Summary List workspaces
// @ID get-workspaces
// @ID list-workspaces
// @Security CoderSessionToken
// @Produce json
// @Tags Workspaces
@@ -113,9 +116,6 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
// @Param has_agent query string false "Filter by agent status" Enums(connected,connecting,disconnected,timeout)
// @Success 200 {object} codersdk.WorkspacesResponse
// @Router /workspaces [get]
//
// workspaces returns all workspaces a user can read.
// Optional filters with query params
func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
@@ -266,6 +266,8 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)
))
}
// Create a new workspace for the currently authenticated user.
//
// @Summary Create user workspace by organization
// @ID create-user-workspace-by-organization
// @Security CoderSessionToken
@@ -275,8 +277,6 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)
// @Param user path string true "Username, UUID, or me"
// @Success 200 {object} codersdk.Workspace
// @Router /organizations/{organization}/members/{user}/workspaces [post]
//
// Create a new workspace for the currently authenticated user.
func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
@@ -558,7 +558,6 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
// @ID update-workspace-metadata-by-id
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Workspaces
// @Param workspace path string true "Workspace ID" format(uuid)
// @Param request body codersdk.UpdateWorkspaceRequest true "Metadata update request"
@@ -648,7 +647,6 @@ func (api *API) patchWorkspace(rw http.ResponseWriter, r *http.Request) {
// @ID update-workspace-autostart-schedule-by-id
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Workspaces
// @Param workspace path string true "Workspace ID" format(uuid)
// @Param request body codersdk.UpdateWorkspaceAutostartRequest true "Schedule update request"
@@ -711,7 +709,6 @@ func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
// @ID update-workspace-ttl-by-id
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Workspaces
// @Param workspace path string true "Workspace ID" format(uuid)
// @Param request body codersdk.UpdateWorkspaceTTLRequest true "Workspace TTL update request"
@@ -875,7 +872,7 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
}
// @Summary Watch workspace by ID
// @ID watch-workspace-id
// @ID watch-workspace-by-id
// @Security CoderSessionToken
// @Produce text/event-stream
// @Tags Workspaces