mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add endpoint for resolving autostart status (#10507)
This commit is contained in:
Generated
+43
@@ -6480,6 +6480,41 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/resolve-autostart": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Workspaces"
|
||||
],
|
||||
"summary": "Resolve workspace autostart by id.",
|
||||
"operationId": "resolve-workspace-autostart-by-id",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace ID",
|
||||
"name": "workspace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.ResolveAutostartResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/ttl": {
|
||||
"put": {
|
||||
"security": [
|
||||
@@ -9720,6 +9755,14 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ResolveAutostartResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"parameter_mismatch": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ResourceType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
Generated
+39
@@ -5718,6 +5718,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/resolve-autostart": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Workspaces"],
|
||||
"summary": "Resolve workspace autostart by id.",
|
||||
"operationId": "resolve-workspace-autostart-by-id",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace ID",
|
||||
"name": "workspace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.ResolveAutostartResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/ttl": {
|
||||
"put": {
|
||||
"security": [
|
||||
@@ -8756,6 +8787,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ResolveAutostartResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"parameter_mismatch": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ResourceType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -885,6 +885,7 @@ func New(options *Options) *API {
|
||||
r.Put("/extend", api.putExtendWorkspace)
|
||||
r.Put("/dormant", api.putWorkspaceDormant)
|
||||
r.Put("/autoupdates", api.putWorkspaceAutoupdates)
|
||||
r.Get("/resolve-autostart", api.resolveAutostart)
|
||||
})
|
||||
})
|
||||
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/parameter"
|
||||
@@ -30,6 +31,19 @@ func WorkspaceBuildParameter(p database.WorkspaceBuildParameter) codersdk.Worksp
|
||||
}
|
||||
}
|
||||
|
||||
func TemplateVersionParameters(params []database.TemplateVersionParameter) ([]codersdk.TemplateVersionParameter, error) {
|
||||
out := make([]codersdk.TemplateVersionParameter, len(params))
|
||||
var err error
|
||||
for i, p := range params {
|
||||
out[i], err = TemplateVersionParameter(p)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("convert template version parameter %q: %w", p.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func TemplateVersionParameter(param database.TemplateVersionParameter) (codersdk.TemplateVersionParameter, error) {
|
||||
options, err := templateVersionParameterOptions(param.Options)
|
||||
if err != nil {
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
|
||||
@@ -1059,6 +1060,100 @@ func (api *API) putWorkspaceAutoupdates(rw http.ResponseWriter, r *http.Request)
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary Resolve workspace autostart by id.
|
||||
// @ID resolve-workspace-autostart-by-id
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Workspaces
|
||||
// @Param workspace path string true "Workspace ID" format(uuid)
|
||||
// @Success 200 {object} codersdk.ResolveAutostartResponse
|
||||
// @Router /workspaces/{workspace}/resolve-autostart [get]
|
||||
func (api *API) resolveAutostart(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
workspace = httpmw.WorkspaceParam(r)
|
||||
)
|
||||
|
||||
template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
templateAccessControl := (*(api.AccessControlStore.Load())).GetTemplateAccessControl(template)
|
||||
useActiveVersion := templateAccessControl.RequireActiveVersion || workspace.AutomaticUpdates == database.AutomaticUpdatesAlways
|
||||
if !useActiveVersion {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ResolveAutostartResponse{})
|
||||
return
|
||||
}
|
||||
|
||||
build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching latest workspace build.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if build.TemplateVersionID == template.ActiveVersionID {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ResolveAutostartResponse{})
|
||||
return
|
||||
}
|
||||
|
||||
version, err := api.Database.GetTemplateVersionByID(ctx, template.ActiveVersionID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching template version.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
dbVersionParams, err := api.Database.GetTemplateVersionParameters(ctx, version.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching template version parameters.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
dbBuildParams, err := api.Database.GetWorkspaceBuildParameters(ctx, build.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching latest workspace build parameters.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
versionParams, err := db2sdk.TemplateVersionParameters(dbVersionParams)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error converting template version parameters.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
resolver := codersdk.ParameterResolver{
|
||||
Rich: db2sdk.WorkspaceBuildParameters(dbBuildParams),
|
||||
}
|
||||
|
||||
var response codersdk.ResolveAutostartResponse
|
||||
for _, param := range versionParams {
|
||||
_, err := resolver.ValidateResolve(param, nil)
|
||||
// There's a parameter mismatch if we get an error back from the
|
||||
// resolver.
|
||||
response.ParameterMismatch = err != nil
|
||||
if response.ParameterMismatch {
|
||||
break
|
||||
}
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// @Summary Watch workspace by ID
|
||||
// @ID watch-workspace-by-id
|
||||
// @Security CoderSessionToken
|
||||
|
||||
@@ -340,6 +340,99 @@ func TestWorkspace(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolveAutostart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
version1 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, version1.ID)
|
||||
template := coderdtest.CreateTemplate(t, ownerClient, owner.OrganizationID, version1.ID)
|
||||
|
||||
params := &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: []*proto.Response{
|
||||
{
|
||||
Type: &proto.Response_Plan{
|
||||
Plan: &proto.PlanComplete{
|
||||
Parameters: []*proto.RichParameter{
|
||||
{
|
||||
Name: "param",
|
||||
Description: "param",
|
||||
Required: true,
|
||||
Mutable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ProvisionApply: echo.ApplyComplete,
|
||||
}
|
||||
version2 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, params, func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
||||
ctvr.TemplateID = template.ID
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, version2.ID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, owner.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.AutomaticUpdates = codersdk.AutomaticUpdatesAlways
|
||||
})
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
err := ownerClient.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ID: version2.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Autostart shouldn't be possible if parameters do not match.
|
||||
resp, err := client.ResolveAutostart(ctx, workspace.ID.String())
|
||||
require.NoError(t, err)
|
||||
require.True(t, resp.ParameterMismatch)
|
||||
|
||||
update, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
TemplateVersionID: version2.ID,
|
||||
Transition: codersdk.WorkspaceTransitionStart,
|
||||
RichParameterValues: []codersdk.WorkspaceBuildParameter{
|
||||
{
|
||||
Name: "param",
|
||||
Value: "Hello",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, update.ID)
|
||||
|
||||
// We should be able to autostart since parameters are updated.
|
||||
resp, err = client.ResolveAutostart(ctx, workspace.ID.String())
|
||||
require.NoError(t, err)
|
||||
require.False(t, resp.ParameterMismatch)
|
||||
|
||||
// Create one last version where the parameters are the same as the previous
|
||||
// version.
|
||||
version3 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, params, func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
||||
ctvr.TemplateID = template.ID
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, version3.ID)
|
||||
|
||||
err = ownerClient.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ID: version3.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Even though we're out of date we should still be able to autostart
|
||||
// since parameters resolve.
|
||||
resp, err = client.ResolveAutostart(ctx, workspace.ID.String())
|
||||
require.NoError(t, err)
|
||||
require.False(t, resp.ParameterMismatch)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdminViewAllWorkspaces(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
|
||||
@@ -449,6 +449,23 @@ func (c *Client) WorkspaceQuota(ctx context.Context, userID string) (WorkspaceQu
|
||||
return quota, json.NewDecoder(res.Body).Decode("a)
|
||||
}
|
||||
|
||||
type ResolveAutostartResponse struct {
|
||||
ParameterMismatch bool `json:"parameter_mismatch"`
|
||||
}
|
||||
|
||||
func (c *Client) ResolveAutostart(ctx context.Context, workspaceID string) (ResolveAutostartResponse, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/resolve-autostart", workspaceID), nil)
|
||||
if err != nil {
|
||||
return ResolveAutostartResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return ResolveAutostartResponse{}, ReadBodyAsError(res)
|
||||
}
|
||||
var response ResolveAutostartResponse
|
||||
return response, json.NewDecoder(res.Body).Decode(&response)
|
||||
}
|
||||
|
||||
// WorkspaceNotifyChannel is the PostgreSQL NOTIFY
|
||||
// channel to listen for updates on. The payload is empty,
|
||||
// because the size of a workspace payload can be very large.
|
||||
|
||||
Generated
+14
@@ -4100,6 +4100,20 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
| `region_id` | integer | false | | Region ID is the region of the replica. |
|
||||
| `relay_address` | string | false | | Relay address is the accessible address to relay DERP connections. |
|
||||
|
||||
## codersdk.ResolveAutostartResponse
|
||||
|
||||
```json
|
||||
{
|
||||
"parameter_mismatch": true
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------------- | ------- | -------- | ------------ | ----------- |
|
||||
| `parameter_mismatch` | boolean | false | | |
|
||||
|
||||
## codersdk.ResourceType
|
||||
|
||||
```json
|
||||
|
||||
Generated
+37
@@ -1240,6 +1240,43 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/extend \
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Resolve workspace autostart by id.
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/resolve-autostart \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /workspaces/{workspace}/resolve-autostart`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| ----------- | ---- | ------------ | -------- | ------------ |
|
||||
| `workspace` | path | string(uuid) | true | Workspace ID |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"parameter_mismatch": true
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------- |
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.ResolveAutostartResponse](schemas.md#codersdkresolveautostartresponse) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Update workspace TTL by ID
|
||||
|
||||
### Code samples
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/schedule"
|
||||
"github.com/coder/coder/v2/provisioner/echo"
|
||||
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
@@ -1070,6 +1071,72 @@ func TestWorkspaceLock(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolveAutostart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
TemplateScheduleStore: &schedule.EnterpriseTemplateScheduleStore{},
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAccessControl: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
version1 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, version1.ID)
|
||||
template := coderdtest.CreateTemplate(t, ownerClient, owner.OrganizationID, version1.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.RequireActiveVersion = true
|
||||
})
|
||||
|
||||
params := &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: []*proto.Response{
|
||||
{
|
||||
Type: &proto.Response_Plan{
|
||||
Plan: &proto.PlanComplete{
|
||||
Parameters: []*proto.RichParameter{
|
||||
{
|
||||
Name: "param",
|
||||
Description: "param",
|
||||
Required: true,
|
||||
Mutable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ProvisionApply: echo.ApplyComplete,
|
||||
}
|
||||
version2 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, params, func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
||||
ctvr.TemplateID = template.ID
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, version2.ID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, owner.OrganizationID, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
//nolint:gocritic
|
||||
err := ownerClient.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ID: version2.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Autostart shouldn't be possible since the template requires automatic
|
||||
// updates.
|
||||
resp, err := client.ResolveAutostart(ctx, workspace.ID.String())
|
||||
require.NoError(t, err)
|
||||
require.True(t, resp.ParameterMismatch)
|
||||
}
|
||||
|
||||
func must[T any](value T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
Generated
+5
@@ -812,6 +812,11 @@ export interface Replica {
|
||||
readonly database_latency: number;
|
||||
}
|
||||
|
||||
// From codersdk/workspaces.go
|
||||
export interface ResolveAutostartResponse {
|
||||
readonly parameter_mismatch: boolean;
|
||||
}
|
||||
|
||||
// From codersdk/client.go
|
||||
export interface Response {
|
||||
readonly message: string;
|
||||
|
||||
Reference in New Issue
Block a user