mirror of
https://github.com/coder/coder.git
synced 2025-07-30 22:19:53 +00:00
feat: make dynamic parameters respect owner in form (#18013)
Closes https://github.com/coder/coder/issues/18012 --------- Co-authored-by: Jaayden Halko <jaayden.halko@gmail.com>
This commit is contained in:
74
coderd/apidoc/docs.go
generated
74
coderd/apidoc/docs.go
generated
@@ -5880,6 +5880,43 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/dynamic-parameters": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Templates"
|
||||
],
|
||||
"summary": "Open dynamic parameters WebSocket by template version",
|
||||
"operationId": "open-dynamic-parameters-websocket-by-template-version",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "templateversion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"101": {
|
||||
"description": "Switching Protocols"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/external-auth": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -7735,43 +7772,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{user}/templateversions/{templateversion}/parameters": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Templates"
|
||||
],
|
||||
"summary": "Open dynamic parameters WebSocket by template version",
|
||||
"operationId": "open-dynamic-parameters-websocket-by-template-version",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "templateversion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"101": {
|
||||
"description": "Switching Protocols"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{user}/webpush/subscription": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
70
coderd/apidoc/swagger.json
generated
70
coderd/apidoc/swagger.json
generated
@@ -5197,6 +5197,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/dynamic-parameters": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": ["Templates"],
|
||||
"summary": "Open dynamic parameters WebSocket by template version",
|
||||
"operationId": "open-dynamic-parameters-websocket-by-template-version",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "templateversion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"101": {
|
||||
"description": "Switching Protocols"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/external-auth": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -6834,41 +6869,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{user}/templateversions/{templateversion}/parameters": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": ["Templates"],
|
||||
"summary": "Open dynamic parameters WebSocket by template version",
|
||||
"operationId": "open-dynamic-parameters-websocket-by-template-version",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "templateversion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"101": {
|
||||
"description": "Switching Protocols"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{user}/webpush/subscription": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
@@ -1122,6 +1122,7 @@ func New(options *Options) *API {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
r.Route("/templateversions/{templateversion}", func(r chi.Router) {
|
||||
r.Use(
|
||||
apiKeyMiddleware,
|
||||
@@ -1150,6 +1151,13 @@ func New(options *Options) *API {
|
||||
r.Get("/{jobID}/matched-provisioners", api.templateVersionDryRunMatchedProvisioners)
|
||||
r.Patch("/{jobID}/cancel", api.patchTemplateVersionDryRunCancel)
|
||||
})
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(
|
||||
httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentDynamicParameters),
|
||||
)
|
||||
r.Get("/dynamic-parameters", api.templateVersionDynamicParameters)
|
||||
})
|
||||
})
|
||||
r.Route("/users", func(r chi.Router) {
|
||||
r.Get("/first", api.firstUser)
|
||||
@@ -1210,19 +1218,6 @@ func New(options *Options) *API {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(httpmw.ExtractUserParam(options.Database))
|
||||
|
||||
// Similarly to creating a workspace, evaluating parameters for a
|
||||
// new workspace should also match the authz story of
|
||||
// postWorkspacesByOrganization
|
||||
// TODO: Do not require site wide read user permission. Make this work
|
||||
// with org member permissions.
|
||||
r.Route("/templateversions/{templateversion}", func(r chi.Router) {
|
||||
r.Use(
|
||||
httpmw.ExtractTemplateVersionParam(options.Database),
|
||||
httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentDynamicParameters),
|
||||
)
|
||||
r.Get("/parameters", api.templateVersionDynamicParameters)
|
||||
})
|
||||
|
||||
r.Post("/convert-login", api.postConvertLoginType)
|
||||
r.Delete("/", api.deleteUser)
|
||||
r.Get("/", api.userByName)
|
||||
|
@@ -36,7 +36,7 @@ import (
|
||||
// @Param user path string true "Template version ID" format(uuid)
|
||||
// @Param templateversion path string true "Template version ID" format(uuid)
|
||||
// @Success 101
|
||||
// @Router /users/{user}/templateversions/{templateversion}/parameters [get]
|
||||
// @Router /templateversions/{templateversion}/dynamic-parameters [get]
|
||||
func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
templateVersion := httpmw.TemplateVersionParam(r)
|
||||
@@ -77,12 +77,12 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
|
||||
}
|
||||
}
|
||||
|
||||
type previewFunction func(ctx context.Context, values map[string]string) (*preview.Output, hcl.Diagnostics)
|
||||
type previewFunction func(ctx context.Context, ownerID uuid.UUID, values map[string]string) (*preview.Output, hcl.Diagnostics)
|
||||
|
||||
func (api *API) handleDynamicParameters(rw http.ResponseWriter, r *http.Request, tf database.TemplateVersionTerraformValue, templateVersion database.TemplateVersion) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
user = httpmw.UserParam(r)
|
||||
ctx = r.Context()
|
||||
apikey = httpmw.APIKey(r)
|
||||
)
|
||||
|
||||
// nolint:gocritic // We need to fetch the templates files for the Terraform
|
||||
@@ -130,7 +130,7 @@ func (api *API) handleDynamicParameters(rw http.ResponseWriter, r *http.Request,
|
||||
templateFS = files.NewOverlayFS(templateFS, []files.Overlay{{Path: ".terraform/modules", FS: moduleFilesFS}})
|
||||
}
|
||||
|
||||
owner, err := getWorkspaceOwnerData(ctx, api.Database, user, templateVersion.OrganizationID)
|
||||
owner, err := getWorkspaceOwnerData(ctx, api.Database, apikey.UserID, templateVersion.OrganizationID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace owner.",
|
||||
@@ -145,10 +145,46 @@ func (api *API) handleDynamicParameters(rw http.ResponseWriter, r *http.Request,
|
||||
Owner: owner,
|
||||
}
|
||||
|
||||
api.handleParameterWebsocket(rw, r, func(ctx context.Context, values map[string]string) (*preview.Output, hcl.Diagnostics) {
|
||||
// failedOwners keeps track of which owners failed to fetch from the database.
|
||||
// This prevents db spam on repeated requests for the same failed owner.
|
||||
failedOwners := make(map[uuid.UUID]error)
|
||||
failedOwnerDiag := hcl.Diagnostics{
|
||||
{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Failed to fetch workspace owner",
|
||||
Detail: "Please check your permissions or the user may not exist.",
|
||||
Extra: previewtypes.DiagnosticExtra{
|
||||
Code: "owner_not_found",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
api.handleParameterWebsocket(rw, r, apikey.UserID, func(ctx context.Context, ownerID uuid.UUID, values map[string]string) (*preview.Output, hcl.Diagnostics) {
|
||||
if ownerID == uuid.Nil {
|
||||
// Default to the authenticated user
|
||||
// Nice for testing
|
||||
ownerID = apikey.UserID
|
||||
}
|
||||
|
||||
if _, ok := failedOwners[ownerID]; ok {
|
||||
// If it has failed once, assume it will fail always.
|
||||
// Re-open the websocket to try again.
|
||||
return nil, failedOwnerDiag
|
||||
}
|
||||
|
||||
// Update the input values with the new values.
|
||||
// The rest of the input is unchanged.
|
||||
input.ParameterValues = values
|
||||
|
||||
// Update the owner if there is a change
|
||||
if input.Owner.ID != ownerID.String() {
|
||||
owner, err = getWorkspaceOwnerData(ctx, api.Database, ownerID, templateVersion.OrganizationID)
|
||||
if err != nil {
|
||||
failedOwners[ownerID] = err
|
||||
return nil, failedOwnerDiag
|
||||
}
|
||||
input.Owner = owner
|
||||
}
|
||||
|
||||
return preview.Preview(ctx, input, templateFS)
|
||||
})
|
||||
}
|
||||
@@ -239,7 +275,7 @@ func (api *API) handleStaticParameters(rw http.ResponseWriter, r *http.Request,
|
||||
params = append(params, param)
|
||||
}
|
||||
|
||||
api.handleParameterWebsocket(rw, r, func(_ context.Context, values map[string]string) (*preview.Output, hcl.Diagnostics) {
|
||||
api.handleParameterWebsocket(rw, r, uuid.Nil, func(_ context.Context, _ uuid.UUID, values map[string]string) (*preview.Output, hcl.Diagnostics) {
|
||||
for i := range params {
|
||||
param := ¶ms[i]
|
||||
paramValue, ok := values[param.Name]
|
||||
@@ -264,7 +300,7 @@ func (api *API) handleStaticParameters(rw http.ResponseWriter, r *http.Request,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) handleParameterWebsocket(rw http.ResponseWriter, r *http.Request, render previewFunction) {
|
||||
func (api *API) handleParameterWebsocket(rw http.ResponseWriter, r *http.Request, ownerID uuid.UUID, render previewFunction) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
@@ -284,7 +320,7 @@ func (api *API) handleParameterWebsocket(rw http.ResponseWriter, r *http.Request
|
||||
)
|
||||
|
||||
// Send an initial form state, computed without any user input.
|
||||
result, diagnostics := render(ctx, map[string]string{})
|
||||
result, diagnostics := render(ctx, ownerID, map[string]string{})
|
||||
response := codersdk.DynamicParametersResponse{
|
||||
ID: -1, // Always start with -1.
|
||||
Diagnostics: db2sdk.HCLDiagnostics(diagnostics),
|
||||
@@ -312,7 +348,7 @@ func (api *API) handleParameterWebsocket(rw http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
result, diagnostics := render(ctx, update.Inputs)
|
||||
result, diagnostics := render(ctx, update.OwnerID, update.Inputs)
|
||||
response := codersdk.DynamicParametersResponse{
|
||||
ID: update.ID,
|
||||
Diagnostics: db2sdk.HCLDiagnostics(diagnostics),
|
||||
@@ -332,17 +368,24 @@ func (api *API) handleParameterWebsocket(rw http.ResponseWriter, r *http.Request
|
||||
func getWorkspaceOwnerData(
|
||||
ctx context.Context,
|
||||
db database.Store,
|
||||
user database.User,
|
||||
ownerID uuid.UUID,
|
||||
organizationID uuid.UUID,
|
||||
) (previewtypes.WorkspaceOwner, error) {
|
||||
var g errgroup.Group
|
||||
|
||||
// TODO: @emyrk we should only need read access on the org member, not the
|
||||
// site wide user object. Figure out a better way to handle this.
|
||||
user, err := db.GetUserByID(ctx, ownerID)
|
||||
if err != nil {
|
||||
return previewtypes.WorkspaceOwner{}, xerrors.Errorf("fetch user: %w", err)
|
||||
}
|
||||
|
||||
var ownerRoles []previewtypes.WorkspaceOwnerRBACRole
|
||||
g.Go(func() error {
|
||||
// nolint:gocritic // This is kind of the wrong query to use here, but it
|
||||
// matches how the provisioner currently works. We should figure out
|
||||
// something that needs less escalation but has the correct behavior.
|
||||
row, err := db.GetAuthorizationUserRoles(dbauthz.AsSystemRestricted(ctx), user.ID)
|
||||
row, err := db.GetAuthorizationUserRoles(dbauthz.AsSystemRestricted(ctx), ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -372,7 +415,7 @@ func getWorkspaceOwnerData(
|
||||
// The correct public key has to be sent. This will not be leaked
|
||||
// unless the template leaks it.
|
||||
// nolint:gocritic
|
||||
key, err := db.GetGitSSHKey(dbauthz.AsSystemRestricted(ctx), user.ID)
|
||||
key, err := db.GetGitSSHKey(dbauthz.AsSystemRestricted(ctx), ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -388,7 +431,7 @@ func getWorkspaceOwnerData(
|
||||
// nolint:gocritic
|
||||
groups, err := db.GetGroups(dbauthz.AsSystemRestricted(ctx), database.GetGroupsParams{
|
||||
OrganizationID: organizationID,
|
||||
HasMemberID: user.ID,
|
||||
HasMemberID: ownerID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -400,7 +443,7 @@ func getWorkspaceOwnerData(
|
||||
return nil
|
||||
})
|
||||
|
||||
err := g.Wait()
|
||||
err = g.Wait()
|
||||
if err != nil {
|
||||
return previewtypes.WorkspaceOwner{}, err
|
||||
}
|
||||
|
@@ -32,7 +32,7 @@ func TestDynamicParametersOwnerSSHPublicKey(t *testing.T) {
|
||||
cfg.Experiments = []string{string(codersdk.ExperimentDynamicParameters)}
|
||||
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, DeploymentValues: cfg})
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
templateAdmin, templateAdminUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
templateAdmin, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/public_key/main.tf")
|
||||
require.NoError(t, err)
|
||||
@@ -57,7 +57,7 @@ func TestDynamicParametersOwnerSSHPublicKey(t *testing.T) {
|
||||
_ = coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, templateAdminUser.ID, version.ID)
|
||||
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, version.ID)
|
||||
require.NoError(t, err)
|
||||
defer stream.Close(websocket.StatusGoingAway)
|
||||
|
||||
@@ -210,6 +210,47 @@ func TestDynamicParametersWithTerraformValues(t *testing.T) {
|
||||
// test to make it obvious what this test is doing.
|
||||
require.Zero(t, setup.api.FileCache.Count())
|
||||
})
|
||||
|
||||
t.Run("BadOwner", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/modules/main.tf")
|
||||
require.NoError(t, err)
|
||||
|
||||
modulesArchive, err := terraform.GetModulesArchive(os.DirFS("testdata/parameters/modules"))
|
||||
require.NoError(t, err)
|
||||
|
||||
setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
|
||||
provisionerDaemonVersion: provProto.CurrentVersion.String(),
|
||||
mainTF: dynamicParametersTerraformSource,
|
||||
modulesArchive: modulesArchive,
|
||||
plan: nil,
|
||||
static: nil,
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
stream := setup.stream
|
||||
previews := stream.Chan()
|
||||
|
||||
// Should see the output of the module represented
|
||||
preview := testutil.RequireReceive(ctx, t, previews)
|
||||
require.Equal(t, -1, preview.ID)
|
||||
require.Empty(t, preview.Diagnostics)
|
||||
|
||||
err = stream.Send(codersdk.DynamicParametersRequest{
|
||||
ID: 1,
|
||||
Inputs: map[string]string{
|
||||
"jetbrains_ide": "GO",
|
||||
},
|
||||
OwnerID: uuid.New(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
preview = testutil.RequireReceive(ctx, t, previews)
|
||||
require.Equal(t, 1, preview.ID)
|
||||
require.Len(t, preview.Diagnostics, 1)
|
||||
require.Equal(t, preview.Diagnostics[0].Extra.Code, "owner_not_found")
|
||||
})
|
||||
}
|
||||
|
||||
type setupDynamicParamsTestParams struct {
|
||||
@@ -242,7 +283,7 @@ func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dyn
|
||||
})
|
||||
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
templateAdmin, templateAdminUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
templateAdmin, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
files := echo.WithExtraFiles(map[string][]byte{
|
||||
"main.tf": args.mainTF,
|
||||
@@ -262,7 +303,7 @@ func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dyn
|
||||
_ = coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, templateAdminUser.ID, version.ID)
|
||||
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, version.ID)
|
||||
if args.expectWebsocketError {
|
||||
require.Errorf(t, err, "expected error forming websocket")
|
||||
} else {
|
||||
|
@@ -114,6 +114,8 @@ type DynamicParametersRequest struct {
|
||||
// ID so that the client can match it to the request.
|
||||
ID int `json:"id"`
|
||||
Inputs map[string]string `json:"inputs"`
|
||||
// OwnerID if uuid.Nil, it defaults to `codersdk.Me`
|
||||
OwnerID uuid.UUID `json:"owner_id,omitempty" format:"uuid"`
|
||||
}
|
||||
|
||||
type DynamicParametersResponse struct {
|
||||
@@ -123,8 +125,8 @@ type DynamicParametersResponse struct {
|
||||
// TODO: Workspace tags
|
||||
}
|
||||
|
||||
func (c *Client) TemplateVersionDynamicParameters(ctx context.Context, userID, version uuid.UUID) (*wsjson.Stream[DynamicParametersResponse, DynamicParametersRequest], error) {
|
||||
conn, err := c.Dial(ctx, fmt.Sprintf("/api/v2/users/%s/templateversions/%s/parameters", userID, version), nil)
|
||||
func (c *Client) TemplateVersionDynamicParameters(ctx context.Context, version uuid.UUID) (*wsjson.Stream[DynamicParametersResponse, DynamicParametersRequest], error) {
|
||||
conn, err := c.Dial(ctx, fmt.Sprintf("/api/v2/templateversions/%s/dynamic-parameters", version), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
54
docs/reference/api/templates.md
generated
54
docs/reference/api/templates.md
generated
@@ -2577,6 +2577,33 @@ Status Code **200**
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Open dynamic parameters WebSocket by template version
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/dynamic-parameters \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /templateversions/{templateversion}/dynamic-parameters`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|-------------------|------|--------------|----------|---------------------|
|
||||
| `user` | path | string(uuid) | true | Template version ID |
|
||||
| `templateversion` | path | string(uuid) | true | Template version ID |
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|--------------------------------------------------------------------------|---------------------|--------|
|
||||
| 101 | [Switching Protocols](https://tools.ietf.org/html/rfc7231#section-6.2.2) | Switching Protocols | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get external auth by template version
|
||||
|
||||
### Code samples
|
||||
@@ -3344,30 +3371,3 @@ Status Code **200**
|
||||
| `type` | `bool` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Open dynamic parameters WebSocket by template version
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/users/{user}/templateversions/{templateversion}/parameters \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /users/{user}/templateversions/{templateversion}/parameters`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|-------------------|------|--------------|----------|---------------------|
|
||||
| `user` | path | string(uuid) | true | Template version ID |
|
||||
| `templateversion` | path | string(uuid) | true | Template version ID |
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|--------------------------------------------------------------------------|---------------------|--------|
|
||||
| 101 | [Switching Protocols](https://tools.ietf.org/html/rfc7231#section-6.2.2) | Switching Protocols | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
@@ -59,7 +59,7 @@ func TestDynamicParametersOwnerGroups(t *testing.T) {
|
||||
_ = coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, templateAdminUser.ID, version.ID)
|
||||
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, version.ID)
|
||||
require.NoError(t, err)
|
||||
defer stream.Close(websocket.StatusGoingAway)
|
||||
|
||||
|
@@ -1000,7 +1000,6 @@ class ApiMethods {
|
||||
};
|
||||
|
||||
templateVersionDynamicParameters = (
|
||||
userId: string,
|
||||
versionId: string,
|
||||
{
|
||||
onMessage,
|
||||
@@ -1013,7 +1012,7 @@ class ApiMethods {
|
||||
},
|
||||
): WebSocket => {
|
||||
const socket = createWebSocket(
|
||||
`/api/v2/users/${userId}/templateversions/${versionId}/parameters`,
|
||||
`/api/v2/templateversions/${versionId}/dynamic-parameters`,
|
||||
);
|
||||
|
||||
socket.addEventListener("message", (event) =>
|
||||
|
1
site/src/api/typesGenerated.ts
generated
1
site/src/api/typesGenerated.ts
generated
@@ -774,6 +774,7 @@ export const DisplayApps: DisplayApp[] = [
|
||||
export interface DynamicParametersRequest {
|
||||
readonly id: number;
|
||||
readonly inputs: Record<string, string>;
|
||||
readonly owner_id?: string;
|
||||
}
|
||||
|
||||
// From codersdk/parameters.go
|
||||
|
@@ -88,16 +88,19 @@ const CreateWorkspacePageExperimental: FC = () => {
|
||||
|
||||
const autofillParameters = getAutofillParameters(searchParams);
|
||||
|
||||
const sendMessage = useCallback((formValues: Record<string, string>) => {
|
||||
const request: DynamicParametersRequest = {
|
||||
id: wsResponseId.current + 1,
|
||||
inputs: formValues,
|
||||
};
|
||||
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
|
||||
ws.current.send(JSON.stringify(request));
|
||||
wsResponseId.current = wsResponseId.current + 1;
|
||||
}
|
||||
}, []);
|
||||
const sendMessage = useEffectEvent(
|
||||
(formValues: Record<string, string>, ownerId?: string) => {
|
||||
const request: DynamicParametersRequest = {
|
||||
id: wsResponseId.current + 1,
|
||||
owner_id: ownerId ?? owner.id,
|
||||
inputs: formValues,
|
||||
};
|
||||
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
|
||||
ws.current.send(JSON.stringify(request));
|
||||
wsResponseId.current = wsResponseId.current + 1;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// On page load, sends all initial parameter values to the websocket
|
||||
// (including defaults and autofilled from the url)
|
||||
@@ -145,35 +148,31 @@ const CreateWorkspacePageExperimental: FC = () => {
|
||||
useEffect(() => {
|
||||
if (!realizedVersionId) return;
|
||||
|
||||
const socket = API.templateVersionDynamicParameters(
|
||||
owner.id,
|
||||
realizedVersionId,
|
||||
{
|
||||
onMessage,
|
||||
onError: (error) => {
|
||||
if (ws.current === socket) {
|
||||
setWsError(error);
|
||||
}
|
||||
},
|
||||
onClose: () => {
|
||||
if (ws.current === socket) {
|
||||
setWsError(
|
||||
new DetailedError(
|
||||
"Websocket connection for dynamic parameters unexpectedly closed.",
|
||||
"Refresh the page to reset the form.",
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
const socket = API.templateVersionDynamicParameters(realizedVersionId, {
|
||||
onMessage,
|
||||
onError: (error) => {
|
||||
if (ws.current === socket) {
|
||||
setWsError(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
onClose: () => {
|
||||
if (ws.current === socket) {
|
||||
setWsError(
|
||||
new DetailedError(
|
||||
"Websocket connection for dynamic parameters unexpectedly closed.",
|
||||
"Refresh the page to reset the form.",
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
ws.current = socket;
|
||||
|
||||
return () => {
|
||||
socket.close();
|
||||
};
|
||||
}, [owner.id, realizedVersionId, onMessage]);
|
||||
}, [realizedVersionId, onMessage]);
|
||||
|
||||
const organizationId = templateQuery.data?.organization_id;
|
||||
|
||||
|
@@ -79,7 +79,7 @@ interface CreateWorkspacePageViewExperimentalProps {
|
||||
owner: TypesGen.User,
|
||||
) => void;
|
||||
resetMutation: () => void;
|
||||
sendMessage: (message: Record<string, string>) => void;
|
||||
sendMessage: (message: Record<string, string>, ownerId?: string) => void;
|
||||
startPollingExternalAuth: () => void;
|
||||
owner: TypesGen.User;
|
||||
setOwner: (user: TypesGen.User) => void;
|
||||
@@ -280,9 +280,10 @@ export const CreateWorkspacePageViewExperimental: FC<
|
||||
form.values.rich_parameter_values,
|
||||
]);
|
||||
|
||||
// send the last user modified parameter and all touched parameters to the websocket
|
||||
// include any modified parameters and all touched parameters to the websocket request
|
||||
const sendDynamicParamsRequest = (
|
||||
parameters: Array<{ parameter: PreviewParameter; value: string }>,
|
||||
ownerId?: string,
|
||||
) => {
|
||||
const formInputs: Record<string, string> = {};
|
||||
const formParameters = form.values.rich_parameter_values ?? [];
|
||||
@@ -303,7 +304,12 @@ export const CreateWorkspacePageViewExperimental: FC<
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(formInputs);
|
||||
sendMessage(formInputs, ownerId);
|
||||
};
|
||||
|
||||
const handleOwnerChange = (user: TypesGen.User) => {
|
||||
setOwner(user);
|
||||
sendDynamicParamsRequest([], user.id);
|
||||
};
|
||||
|
||||
const handleChange = async (
|
||||
@@ -486,7 +492,7 @@ export const CreateWorkspacePageViewExperimental: FC<
|
||||
<UserAutocomplete
|
||||
value={owner}
|
||||
onChange={(user) => {
|
||||
setOwner(user ?? defaultOwner);
|
||||
handleOwnerChange(user ?? defaultOwner);
|
||||
}}
|
||||
size="medium"
|
||||
/>
|
||||
|
@@ -21,14 +21,7 @@ import {
|
||||
import { useEffectEvent } from "hooks/hookPolyfills";
|
||||
import { CircleHelp, Undo2 } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useMutation, useQuery } from "react-query";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@@ -53,16 +46,17 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
||||
const ws = useRef<WebSocket | null>(null);
|
||||
const [wsError, setWsError] = useState<Error | null>(null);
|
||||
|
||||
const sendMessage = useCallback((formValues: Record<string, string>) => {
|
||||
const sendMessage = useEffectEvent((formValues: Record<string, string>) => {
|
||||
const request: DynamicParametersRequest = {
|
||||
id: wsResponseId.current + 1,
|
||||
owner_id: workspace.owner_id,
|
||||
inputs: formValues,
|
||||
};
|
||||
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
|
||||
ws.current.send(JSON.stringify(request));
|
||||
wsResponseId.current = wsResponseId.current + 1;
|
||||
}
|
||||
}, []);
|
||||
});
|
||||
|
||||
const onMessage = useEffectEvent((response: DynamicParametersResponse) => {
|
||||
if (latestResponse && latestResponse?.id >= response.id) {
|
||||
@@ -76,7 +70,6 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
||||
if (!workspace.latest_build.template_version_id) return;
|
||||
|
||||
const socket = API.templateVersionDynamicParameters(
|
||||
workspace.owner_id,
|
||||
workspace.latest_build.template_version_id,
|
||||
{
|
||||
onMessage,
|
||||
@@ -103,11 +96,7 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
||||
return () => {
|
||||
socket.close();
|
||||
};
|
||||
}, [
|
||||
workspace.owner_id,
|
||||
workspace.latest_build.template_version_id,
|
||||
onMessage,
|
||||
]);
|
||||
}, [workspace.latest_build.template_version_id, onMessage]);
|
||||
|
||||
const updateParameters = useMutation({
|
||||
mutationFn: (buildParameters: WorkspaceBuildParameter[]) =>
|
||||
|
Reference in New Issue
Block a user