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:
Marcin Tojek
2022-12-19 18:43:46 +01:00
committed by GitHub
parent f239ca7ee3
commit dc6d271293
41 changed files with 9272 additions and 76 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+31 -1
View File
@@ -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
}
+78
View File
@@ -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))
})
}
+3
View File
@@ -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,
}
}
+52
View File
@@ -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()
+89
View File
@@ -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)