Skip to content

docs: API workspace agents and builds #5538

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 20 commits into from
Jan 5, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
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
1,055 changes: 1,037 additions & 18 deletions coderd/apidoc/docs.go

Large diffs are not rendered by default.

961 changes: 936 additions & 25 deletions coderd/apidoc/swagger.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions coderd/gitsshkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ 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
// @Router /workspaceagents/me/gitsshkey [get]
func (api *API) agentGitSSHKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
agent := httpmw.WorkspaceAgent(r)
Expand Down
55 changes: 54 additions & 1 deletion coderd/workspaceagents.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, apiAgent)
}

// @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
// @Router /workspaceagents/me/metadata [get]
func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceAgent := httpmw.WorkspaceAgent(r)
Expand Down Expand Up @@ -138,6 +146,15 @@ func (api *API) workspaceAgentMetadata(rw http.ResponseWriter, r *http.Request)
})
}

// @Summary Submit workspace agent version
// @ID submit-workspace-workspace-agent-version
// @Security CoderSessionToken
// @Produce application/json
// @Tags Agents
// @Param request body codersdk.PostWorkspaceAgentVersionRequest true "Version request"
// @Success 200
// @Router /workspaceagents/me/version [post]
// @x-apidocgen {"skip": true}
func (api *API) postWorkspaceAgentVersion(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceAgent := httpmw.WorkspaceAgent(r)
Expand Down Expand Up @@ -438,6 +455,15 @@ 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
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
// @Success 101
// @Router /workspaceagents/me/coordinate [get]
func (api *API) workspaceAgentCoordinate(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()

Expand Down Expand Up @@ -757,6 +783,14 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin
return workspaceAgent, nil
}

// @Summary Submit workspace agent stats
// @ID submit-workspace-workspace-agent-stats
// @Security CoderSessionToken
// @Produce application/json
// @Tags Agents
// @Param request body codersdk.AgentStats true "Stats request"
// @Success 200 {object} codersdk.AgentStatsResponse
// @Router /workspaceagents/me/report-stats [post]
func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()

Expand Down Expand Up @@ -826,6 +860,14 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
})
}

// @Summary Submit workspace application health
// @ID submit-workspace-workspace-agent-health
// @Security CoderSessionToken
// @Produce application/json
// @Tags Agents
// @Param request body codersdk.PostWorkspaceAppHealthsRequest true "Application health request"
// @Success 200
// @Router /workspaceagents/me/app-health [post]
func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceAgent := httpmw.WorkspaceAgent(r)
Expand Down Expand Up @@ -941,8 +983,19 @@ func (api *API) postWorkspaceAppHealth(rw http.ResponseWriter, r *http.Request)
httpapi.Write(ctx, rw, http.StatusOK, nil)
}

// postWorkspaceAgentsGitAuth returns a username and password for use
// workspaceAgentsGitAuth returns a username and password for use
// with GIT_ASKPASS.
//
// @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)
// @Param listen query bool false "Wait for a new token to be issued"
// @Success 200 {object} codersdk.WorkspaceAgentGitAuthResponse
// @Router /workspaceagents/me/gitauth [get]
func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
gitURL := r.URL.Query().Get("url")
Expand Down
68 changes: 68 additions & 0 deletions coderd/workspacebuilds.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ import (
"github.com/coder/coder/codersdk"
)

// @Summary Get workspace build
// @ID get-workspace-build
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
// @Param workspacebuild path string true "Workspace build ID"
// @Success 200 {object} codersdk.WorkspaceBuild
// @Router /workspacebuilds/{workspacebuild} [get]
func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceBuild := httpmw.WorkspaceBuildParam(r)
Expand Down Expand Up @@ -64,6 +72,18 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, apiBuild)
}

// @Summary Get workspace builds by workspace ID
// @ID get-workspace-builds-by-workspace-id
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
// @Param id 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]
func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspace := httpmw.WorkspaceParam(r)
Expand Down Expand Up @@ -254,6 +274,19 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
httpapi.Write(ctx, rw, http.StatusOK, apiBuild)
}

// Azure supports instance identity verification:
// https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#tabgroup_14
//
// @Summary Create workspace build
// @ID create-workspace-build
// @Security CoderSessionToken
// @Accepts json
// @Produce json
// @Tags Builds
// @Param id 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]
func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
Expand Down Expand Up @@ -535,6 +568,14 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusCreated, apiBuild)
}

// @Summary Cancel workspace build
// @ID cancel-workspace-build
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
// @Param workspacebuild path string true "Workspace build ID"
// @Success 200 {object} codersdk.Response
// @Router /workspacebuilds/{workspacebuild}/cancel [patch]
func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceBuild := httpmw.WorkspaceBuildParam(r)
Expand Down Expand Up @@ -630,6 +671,14 @@ func (api *API) verifyUserCanCancelWorkspaceBuilds(ctx context.Context, userID u
return slices.Contains(user.RBACRoles, rbac.RoleOwner()), nil // only user with "owner" role can cancel workspace builds
}

// @Summary Get workspace resources for workspace build
// @ID get-workspace-resources-for-workspace-build
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
// @Param workspacebuild path string true "Workspace build ID"
// @Success 200 {array} codersdk.WorkspaceResource
// @Router /workspacebuilds/{workspacebuild}/resources [get]
func (api *API) workspaceBuildResources(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceBuild := httpmw.WorkspaceBuildParam(r)
Expand Down Expand Up @@ -657,6 +706,17 @@ func (api *API) workspaceBuildResources(rw http.ResponseWriter, r *http.Request)
api.provisionerJobResources(rw, r, job)
}

// @Summary Get workspace build logs
// @ID get-workspace-build-logs
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
// @Param workspacebuild path string true "Workspace build ID"
// @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 /workspacebuilds/{workspacebuild}/logs [get]
func (api *API) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceBuild := httpmw.WorkspaceBuildParam(r)
Expand Down Expand Up @@ -684,6 +744,14 @@ func (api *API) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) {
api.provisionerJobLogs(rw, r, job)
}

// @Summary Get provisioner state for workspace build
// @ID get-provisioner-state-for-workspace-build
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
// @Param workspacebuild path string true "Workspace build ID"
// @Success 200 {object} codersdk.WorkspaceBuild
// @Router /workspacebuilds/{workspacebuild}/state [get]
func (api *API) workspaceBuildState(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspaceBuild := httpmw.WorkspaceBuildParam(r)
Expand Down
27 changes: 27 additions & 0 deletions coderd/workspaceresourceauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ import (

// Azure supports instance identity verification:
// https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#tabgroup_14
//
// @Summary Authenticate agent on Azure instance
// @ID authenticate-agent-on-azure-instance
// @Security CoderSessionToken
// @Produce json
// @Tags Agents
// @Param request body codersdk.AzureInstanceIdentityToken true "Instance identity token"
// @Success 200 {object} codersdk.WorkspaceAgentAuthenticateResponse
// @Router /workspaceagents/azure-instance-identity [post]
func (api *API) postWorkspaceAuthAzureInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req codersdk.AzureInstanceIdentityToken
Expand All @@ -36,6 +45,15 @@ func (api *API) postWorkspaceAuthAzureInstanceIdentity(rw http.ResponseWriter, r
api.handleAuthInstanceID(rw, r, instanceID)
}

// @Summary Authenticate agent on AWS instance
// @ID authenticate-agent-on-aws-instance
// @Security CoderSessionToken
// @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.
Expand All @@ -56,6 +74,15 @@ func (api *API) postWorkspaceAuthAWSInstanceIdentity(rw http.ResponseWriter, r *
api.handleAuthInstanceID(rw, r, identity.InstanceID)
}

// @Summary Authenticate agent on Google Cloud instance
// @ID authenticate-agent-on-google-cloud-instance
// @Security CoderSessionToken
// @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.
Expand Down
22 changes: 12 additions & 10 deletions codersdk/provisionerdaemons.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,24 +66,26 @@ const (
ProvisionerJobFailed ProvisionerJobStatus = "failed"
)

// ProvisionerJob describes the job executed by the provisioning daemon.
type ProvisionerJob struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
StartedAt *time.Time `json:"started_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
CanceledAt *time.Time `json:"canceled_at,omitempty"`
ID uuid.UUID `json:"id" format:"uuid"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
StartedAt *time.Time `json:"started_at,omitempty" format:"date-time"`
CompletedAt *time.Time `json:"completed_at,omitempty" format:"date-time"`
CanceledAt *time.Time `json:"canceled_at,omitempty" format:"date-time"`
Error string `json:"error,omitempty"`
Status ProvisionerJobStatus `json:"status"`
WorkerID *uuid.UUID `json:"worker_id,omitempty"`
FileID uuid.UUID `json:"file_id"`
Status ProvisionerJobStatus `json:"status" enums:"pending,running,succeeded,canceling,canceled,failed"`
WorkerID *uuid.UUID `json:"worker_id,omitempty" format:"uuid"`
FileID uuid.UUID `json:"file_id" format:"uuid"`
Tags map[string]string `json:"tags"`
}

// ProvisionerJobLog represents the provisioner log entry annotated with source and level.
type ProvisionerJobLog struct {
ID int64 `json:"id"`
CreatedAt time.Time `json:"created_at"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
Source LogSource `json:"log_source"`
Level LogLevel `json:"log_level"`
Level LogLevel `json:"log_level" enums:"trace,debug,info,warn,error"`
Stage string `json:"stage"`
Output string `json:"output"`
}
Expand Down
17 changes: 9 additions & 8 deletions codersdk/workspaceagents.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ const (
)

type WorkspaceAgent struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
FirstConnectedAt *time.Time `json:"first_connected_at,omitempty"`
LastConnectedAt *time.Time `json:"last_connected_at,omitempty"`
DisconnectedAt *time.Time `json:"disconnected_at,omitempty"`
Status WorkspaceAgentStatus `json:"status"`
ID uuid.UUID `json:"id" format:"uuid"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps I've asked before, but is there a way to automatically transform these based on the Go type? It seems unfortunate we'd have to tag every new struct, and we will almost certainly miss to add the tag to fields in the future.

It'd be nice if format was reserved as an override since uuid and time.Time are essentially never a different format (would take a custom json Marshaller at which point using format would be appropriate).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid that we would need a fork to apply this feature, let's say, auto-format. It doesn't look like something we can truly solve just with validation.

I'm not sure why it hasn't been implemented this way. Even official examples suggest using format tags.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can certainly understand why it's not the default (due to middlewares and handlers that can do whatever). But it'd be nice to enable such a feature.

Do you think they'd be open to it if we made a PR? Seems like many people could benefit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment, we're using version swaggo v1.8.6, but I can try to bump it to v1.8.9 so that we will also benefit from future PRs. AFAIR v1.8.9 added a lot of noise around all possible values of time.Duration or strange value examples:

-    "value": 0
+    "value": -9223372036854776000

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, if v1.8.9 is problematic we can punt it for now unless you think there's a benefit to upgrading?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me prepare a draft PR and we can discuss the upgrade there.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UpdatedAt time.Time `json:"updated_at" format:"date-time"`
FirstConnectedAt *time.Time `json:"first_connected_at,omitempty" format:"date-time"`
LastConnectedAt *time.Time `json:"last_connected_at,omitempty" format:"date-time"`
DisconnectedAt *time.Time `json:"disconnected_at,omitempty" format:"date-time"`
Status WorkspaceAgentStatus `json:"status" enums:"connecting,connected,disconnected,timeout"`
Name string `json:"name"`
ResourceID uuid.UUID `json:"resource_id"`
ResourceID uuid.UUID `json:"resource_id" format:"uuid"`
InstanceID string `json:"instance_id,omitempty"`
Architecture string `json:"architecture"`
EnvironmentVariables map[string]string `json:"environment_variables"`
Expand Down Expand Up @@ -115,6 +115,7 @@ type WorkspaceAgentConnectionInfo struct {
}

// @typescript-ignore PostWorkspaceAgentVersionRequest
// @Description x-apidocgen:skip
type PostWorkspaceAgentVersionRequest struct {
Version string `json:"version"`
}
Expand Down
4 changes: 2 additions & 2 deletions codersdk/workspaceapps.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const (
)

type WorkspaceApp struct {
ID uuid.UUID `json:"id"`
ID uuid.UUID `json:"id" format:"uuid"`
// URL is the address being proxied to inside the workspace.
// If external is specified, this will be opened on the client.
URL string `json:"url"`
Expand All @@ -42,7 +42,7 @@ type WorkspaceApp struct {
// and there is no app wildcard configured on the server, the app will not
// be accessible in the UI.
Subdomain bool `json:"subdomain"`
SharingLevel WorkspaceAppSharingLevel `json:"sharing_level"`
SharingLevel WorkspaceAppSharingLevel `json:"sharing_level" enums:"owner,authenticated,public"`
// Healthcheck specifies the configuration for checking app health.
Healthcheck Healthcheck `json:"healthcheck"`
Health WorkspaceAppHealth `json:"health"`
Expand Down
5 changes: 4 additions & 1 deletion codersdk/workspacebuilds.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,15 @@ type WorkspaceBuild struct {
InitiatorID uuid.UUID `json:"initiator_id" format:"uuid"`
InitiatorUsername string `json:"initiator_name"`
Job ProvisionerJob `json:"job"`
Reason BuildReason `db:"reason" json:"reason"`
Reason BuildReason `db:"reason" json:"reason" enums:"initiator,autostart,autostop"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! Once we get the docs in shape, it'd be nice to try to process the Go code to find these enums automatically.

Btw, would it be possible to move this enum description to the type? Like:

// BuildReason ...
// @enum initiator,autostart,autostop
type BuildReason string

Perhaps not supported by swaggo.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some magic undocumented extensions like x-enum-varnames or x-enum-descriptions, but I don't think that this is what we're looking for.

See swaggo/swag#984

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, that seems useful with enums that are number based and have string representations in (de-)serialization.

Resources []WorkspaceResource `json:"resources"`
Deadline NullTime `json:"deadline,omitempty" format:"date-time"`
Status WorkspaceStatus `json:"status" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted"`
DailyCost int32 `json:"daily_cost"`
}

// WorkspaceResource describes resources used to create a workspace, for instance:
// containers, images, volumes.
type WorkspaceResource struct {
ID uuid.UUID `json:"id" format:"uuid"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
Expand All @@ -86,6 +88,7 @@ type WorkspaceResource struct {
DailyCost int32 `json:"daily_cost"`
}

// WorkspaceResourceMetadata annotates the workspace resource with custom key-value pairs.
type WorkspaceResourceMetadata struct {
Key string `json:"key"`
Value string `json:"value"`
Expand Down
Loading