mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
feat: Build framework for generating API docs (#5383)
* WIP * Gen * WIP * chi swagger * WIP * WIP * WIP * GetWorkspaces * GetWorkspaces * Markdown * Use widdershins * WIP * WIP * WIP * Markdown template * Fix: makefile * fmt * Fix: comment * Enable swagger conditionally * fix: site * Default false * Flag tests * fix * fix * template fixes * Fix * Fix * Fix * WIP * Formatted * Cleanup * Templates * BEGIN END SECTION * subshell exit code * Fix * Fix merge * WIP * Fix * Fix fmt * Fix * Generic api.md page * Fix merge * Link pages * Fix * Fix * Fix: links * Add icon * Write manifest file * Fix fmt * Fix: enterprise * Fix: Swagger.Enable * Fix: rename apidocs to apidoc * Fix: find -not -prune * Fix: json not available * Fix: rename Coderd API to Coder API * Fix: npm exec * Fix: api dir * Fix: by ID * Fix: string uuid * Fix: include deleted * Fix: indirect go.mod * Fix: source lib.sh * Fix: shellcheck * Fix: pushd popd * Fix: fmt * Fix: improve workspaces * Fix: swagger-enable * Fix * Fix: mention only HTTP 200 * Fix: IDs * Fix: https * Fix: icon * More APis * Fix: format swagger.json * Fix: SwaggerEndpoint * Fix: SCRIPT_DIR * Fix: PROJECT_ROOT * Fix: use code tags in schemas.md * Fix: examples * Fix: examples * Fix: improve format * Fix: date-time,enums * Fix: include_deleted * Fix: array of * Fix: parameter, response * Fix: string time or null * Workspaces: more docs * Workspaces: more docs * Fix: renderDisplayName * Fix: ActiveUserCount * Fix * Fix: typo * Templates: docs * Notice: incomplete
This commit is contained in:
1305
coderd/apidoc/docs.go
Normal file
1305
coderd/apidoc/docs.go
Normal file
File diff suppressed because it is too large
Load Diff
1199
coderd/apidoc/swagger.json
Normal file
1199
coderd/apidoc/swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,7 @@ import (
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/moby/moby/pkg/namesgenerator"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
httpSwagger "github.com/swaggo/http-swagger"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/xerrors"
|
||||
"google.golang.org/api/idtoken"
|
||||
@ -34,6 +35,9 @@ import (
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/buildinfo"
|
||||
|
||||
// Used to serve the Swagger endpoint
|
||||
_ "github.com/coder/coder/coderd/apidoc"
|
||||
"github.com/coder/coder/coderd/audit"
|
||||
"github.com/coder/coder/coderd/awsidentity"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
@ -102,15 +106,34 @@ type Options struct {
|
||||
TailnetCoordinator tailnet.Coordinator
|
||||
DERPServer *derp.Server
|
||||
DERPMap *tailcfg.DERPMap
|
||||
SwaggerEndpoint bool
|
||||
|
||||
MetricsCacheRefreshInterval time.Duration
|
||||
AgentStatsRefreshInterval time.Duration
|
||||
Experimental bool
|
||||
DeploymentConfig *codersdk.DeploymentConfig
|
||||
UpdateCheckOptions *updatecheck.Options // Set non-nil to enable update checking.
|
||||
HTTPClient *http.Client
|
||||
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// @title Coder API
|
||||
// @version 2.0
|
||||
// @description Coderd is the service created by running coder server. It is a thin API that connects workspaces, provisioners and users. coderd stores its state in Postgres and is the only service that communicates with Postgres.
|
||||
// @termsOfService https://coder.com/legal/terms-of-service
|
||||
|
||||
// @contact.name API Support
|
||||
// @contact.url https://coder.com
|
||||
// @contact.email support@coder.com
|
||||
|
||||
// @license.name AGPL-3.0
|
||||
// @license.url https://github.com/coder/coder/blob/main/LICENSE
|
||||
|
||||
// @BasePath /api/v2
|
||||
|
||||
// @securitydefinitions.apiKey CoderSessionToken
|
||||
// @in header
|
||||
// @name Coder-Session-Token
|
||||
// New constructs a Coder API handler.
|
||||
func New(options *Options) *API {
|
||||
if options == nil {
|
||||
@ -578,6 +601,13 @@ func New(options *Options) *API {
|
||||
})
|
||||
})
|
||||
|
||||
if options.SwaggerEndpoint {
|
||||
// Swagger UI requires the URL trailing slash. Otherwise, the browser tries to load /assets
|
||||
// from http://localhost:8080/assets instead of http://localhost:8080/swagger/assets.
|
||||
r.Get("/swagger", http.RedirectHandler("/swagger/", http.StatusTemporaryRedirect).ServeHTTP)
|
||||
r.Get("/swagger/*", httpSwagger.Handler(httpSwagger.URL("/swagger/doc.json")))
|
||||
}
|
||||
|
||||
r.NotFound(compressHandler(http.HandlerFunc(api.siteHandler.ServeHTTP)).ServeHTTP)
|
||||
return api
|
||||
}
|
||||
|
@ -129,3 +129,81 @@ func TestHealthz(t *testing.T) {
|
||||
|
||||
assert.Equal(t, "OK", string(body))
|
||||
}
|
||||
|
||||
func TestSwagger(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const swaggerEndpoint = "/swagger"
|
||||
t.Run("endpoint enabled", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
SwaggerEndpoint: true,
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
|
||||
defer cancel()
|
||||
|
||||
resp, err := requestWithRetries(ctx, t, client, http.MethodGet, swaggerEndpoint, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Contains(t, string(body), "Swagger UI")
|
||||
})
|
||||
t.Run("doc.json exposed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
SwaggerEndpoint: true,
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
|
||||
defer cancel()
|
||||
|
||||
resp, err := requestWithRetries(ctx, t, client, http.MethodGet, swaggerEndpoint+"/doc.json", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Contains(t, string(body), `"swagger": "2.0"`)
|
||||
})
|
||||
t.Run("endpoint disabled by default", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
|
||||
defer cancel()
|
||||
|
||||
resp, err := requestWithRetries(ctx, t, client, http.MethodGet, swaggerEndpoint, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, "<pre>\n</pre>\n", string(body))
|
||||
})
|
||||
t.Run("doc.json disabled by default", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
|
||||
defer cancel()
|
||||
|
||||
resp, err := requestWithRetries(ctx, t, client, http.MethodGet, swaggerEndpoint+"/doc.json", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, "<pre>\n</pre>\n", string(body))
|
||||
})
|
||||
}
|
||||
|
@ -115,6 +115,8 @@ type Options struct {
|
||||
// test instances are running against the same database.
|
||||
Database database.Store
|
||||
Pubsub database.Pubsub
|
||||
|
||||
SwaggerEndpoint bool
|
||||
}
|
||||
|
||||
// New constructs a codersdk client connected to an in-memory API instance.
|
||||
@ -297,6 +299,7 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
|
||||
AgentStatsRefreshInterval: options.AgentStatsRefreshInterval,
|
||||
DeploymentConfig: options.DeploymentConfig,
|
||||
UpdateCheckOptions: options.UpdateCheckOptions,
|
||||
SwaggerEndpoint: options.SwaggerEndpoint,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,14 @@ const (
|
||||
AutoImportTemplateKubernetes AutoImportTemplate = "kubernetes"
|
||||
)
|
||||
|
||||
// @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)
|
||||
// @Success 200 {object} codersdk.Template
|
||||
// @Router /templates/{id} [get]
|
||||
// Returns a single template.
|
||||
func (api *API) template(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
@ -73,6 +81,14 @@ func (api *API) template(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplate(template, count, createdByNameMap[template.ID.String()]))
|
||||
}
|
||||
|
||||
// @Summary Delete template by ID
|
||||
// @ID delete-template-by-id
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Templates
|
||||
// @Param id path string true "Template ID" format(uuid)
|
||||
// @Success 200 {object} codersdk.Response
|
||||
// @Router /templates/{id} [delete]
|
||||
func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
@ -126,6 +142,17 @@ func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Create template by organization
|
||||
// @ID create-template-by-organization
|
||||
// @Security CoderSessionToken
|
||||
// @Consume json
|
||||
// @Produce json
|
||||
// @Tags Templates
|
||||
// @Param request body codersdk.CreateTemplateRequest true "Request body"
|
||||
// @Param organization-id path string true "Organization ID"
|
||||
// @Success 200 {object} codersdk.Template
|
||||
// @Router /organizations/{organization-id}/templates/ [post]
|
||||
// Returns a single template.
|
||||
// Create a new template in an organization.
|
||||
func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
@ -314,6 +341,14 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, template)
|
||||
}
|
||||
|
||||
// @Summary Get templates by organization
|
||||
// @ID get-templates-by-organization
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Templates
|
||||
// @Param organization path string true "Organization ID" format(uuid)
|
||||
// @Success 200 {object} []codersdk.Template
|
||||
// @Router /organizations/{organization}/templates [get]
|
||||
func (api *API) templatesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
@ -372,6 +407,15 @@ func (api *API) templatesByOrganization(rw http.ResponseWriter, r *http.Request)
|
||||
httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplates(templates, workspaceCounts, createdByNameMap))
|
||||
}
|
||||
|
||||
// @Summary Get templates by organization and template name
|
||||
// @ID get-templates-by-organization-and-template-name
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Templates
|
||||
// @Param organization path string true "Organization ID" format(uuid)
|
||||
// @Param template-name path string true "Template name"
|
||||
// @Success 200 {object} codersdk.Template
|
||||
// @Router /organizations/{organization}/templates/{template-name} [get]
|
||||
func (api *API) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
@ -427,6 +471,14 @@ func (api *API) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Re
|
||||
httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplate(template, count, createdByNameMap[template.ID.String()]))
|
||||
}
|
||||
|
||||
// @Summary Update template metadata by ID
|
||||
// @ID update-template-metadata
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Templates
|
||||
// @Param id path string true "Template ID" format(uuid)
|
||||
// @Success 200 {object} codersdk.Template
|
||||
// @Router /templates/{id} [get]
|
||||
func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
|
@ -43,6 +43,15 @@ var (
|
||||
errDeadlineBeforeStart = xerrors.New("new deadline must be before workspace start time")
|
||||
)
|
||||
|
||||
// @Summary Get workspace metadata by ID
|
||||
// @ID get-workspace-metadata-by-id
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Workspaces
|
||||
// @Param id 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]
|
||||
func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
@ -92,6 +101,19 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
|
||||
))
|
||||
}
|
||||
|
||||
// @Summary List workspaces
|
||||
// @ID get-workspaces
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Workspaces
|
||||
// @Param owner query string false "Filter by owner username"
|
||||
// @Param template query string false "Filter by template name"
|
||||
// @Param name query string false "Filter with partial-match by workspace name"
|
||||
// @Param status query string false "Filter by workspace status" Enums(pending,running,stopping,stopped,failed,canceling,canceled,deleted,deleting)
|
||||
// @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) {
|
||||
@ -170,6 +192,16 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Get workspace metadata by owner and workspace name
|
||||
// @ID get-workspace-metadata-by-owner-and-workspace-name
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Workspaces
|
||||
// @Param user path string true "Owner username"
|
||||
// @Param workspacename path string true "Workspace name"
|
||||
// @Param include_deleted query bool false "Return data instead of HTTP 404 if the workspace is deleted"
|
||||
// @Success 200 {object} codersdk.Workspace
|
||||
// @Router /users/{user}/workspace/{workspacename} [get]
|
||||
func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
owner := httpmw.UserParam(r)
|
||||
@ -234,6 +266,15 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)
|
||||
))
|
||||
}
|
||||
|
||||
// @Summary Create workspace by organization
|
||||
// @ID create-workspace-by-organization
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Workspaces
|
||||
// @Param organization path string true "Organization ID" format(uuid)
|
||||
// @Param user path string true "Username"
|
||||
// @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 (
|
||||
@ -520,6 +561,16 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
))
|
||||
}
|
||||
|
||||
// @Summary Update workspace metadata by ID
|
||||
// @ID update-workspace-metadata-by-id
|
||||
// @Security CoderSessionToken
|
||||
// @Consume json
|
||||
// @Produce json
|
||||
// @Tags Workspaces
|
||||
// @Param workspace path string true "Workspace ID" format(uuid)
|
||||
// @Param request body codersdk.UpdateWorkspaceRequest true "Metadata update request"
|
||||
// @Success 204
|
||||
// @Router /workspaces/{workspace} [patch]
|
||||
func (api *API) patchWorkspace(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
@ -600,6 +651,16 @@ func (api *API) patchWorkspace(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary Update workspace autostart schedule by ID
|
||||
// @ID update-workspace-autostart-schedule-by-id
|
||||
// @Security CoderSessionToken
|
||||
// @Consume json
|
||||
// @Produce json
|
||||
// @Tags Workspaces
|
||||
// @Param workspace path string true "Workspace ID" format(uuid)
|
||||
// @Param request body codersdk.UpdateWorkspaceAutostartRequest true "Schedule update request"
|
||||
// @Success 204
|
||||
// @Router /workspaces/{workspace}/autostart [put]
|
||||
func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
@ -653,6 +714,16 @@ func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary Update workspace TTL by ID
|
||||
// @ID update-workspace-ttl-by-id
|
||||
// @Security CoderSessionToken
|
||||
// @Consume json
|
||||
// @Produce json
|
||||
// @Tags Workspaces
|
||||
// @Param workspace path string true "Workspace ID" format(uuid)
|
||||
// @Param request body codersdk.UpdateWorkspaceTTLRequest true "Workspace TTL update request"
|
||||
// @Success 204
|
||||
// @Router /workspaces/{workspace}/ttl [put]
|
||||
func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
@ -719,6 +790,16 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary Extend workspace deadline by ID
|
||||
// @ID extend-workspace-deadline-by-id
|
||||
// @Security CoderSessionToken
|
||||
// @Consume json
|
||||
// @Produce json
|
||||
// @Tags Workspaces
|
||||
// @Param workspace path string true "Workspace ID" format(uuid)
|
||||
// @Param request body codersdk.PutExtendWorkspaceRequest true "Extend deadline update request"
|
||||
// @Success 200 {object} codersdk.Response
|
||||
// @Router /workspaces/{workspace}/extend [put]
|
||||
func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
@ -800,6 +881,14 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(ctx, rw, code, resp)
|
||||
}
|
||||
|
||||
// @Summary Watch workspace by ID
|
||||
// @ID watch-workspace-id
|
||||
// @Security CoderSessionToken
|
||||
// @Produce text/event-stream
|
||||
// @Tags Workspaces
|
||||
// @Param workspace path string true "Workspace ID" format(uuid)
|
||||
// @Success 200 {object} codersdk.Response
|
||||
// @Router /workspaces/{workspace}/watch [get]
|
||||
func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
|
Reference in New Issue
Block a user