|
8 | 8 | "encoding/json"
|
9 | 9 | "errors"
|
10 | 10 | "fmt"
|
| 11 | + "math" |
11 | 12 | "net/http"
|
12 | 13 | "os"
|
13 | 14 |
|
@@ -35,10 +36,15 @@ import (
|
35 | 36 | "github.com/coder/coder/v2/coderd/tracing"
|
36 | 37 | "github.com/coder/coder/v2/coderd/util/ptr"
|
37 | 38 | "github.com/coder/coder/v2/codersdk"
|
| 39 | + "github.com/coder/coder/v2/codersdk/wsjson" |
38 | 40 | "github.com/coder/coder/v2/examples"
|
39 | 41 | "github.com/coder/coder/v2/provisioner/terraform/tfparse"
|
40 | 42 | "github.com/coder/coder/v2/provisionersdk"
|
41 | 43 | sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
|
| 44 | + "github.com/coder/preview" |
| 45 | + previewtypes "github.com/coder/preview/types" |
| 46 | + previewweb "github.com/coder/preview/web" |
| 47 | + "github.com/coder/websocket" |
42 | 48 | )
|
43 | 49 |
|
44 | 50 | // @Summary Get template version by ID
|
@@ -266,6 +272,110 @@ func (api *API) patchCancelTemplateVersion(rw http.ResponseWriter, r *http.Reque
|
266 | 272 | })
|
267 | 273 | }
|
268 | 274 |
|
| 275 | +// @Summary Open dynamic parameters WebSocket by template version |
| 276 | +// @ID open-dynamic-parameters-websocket-by-template-version |
| 277 | +// @Security CoderSessionToken |
| 278 | +// @Produce json |
| 279 | +// @Tags Templates |
| 280 | +// @Param templateversion path string true "Template version ID" format(uuid) |
| 281 | +// @Success 101 |
| 282 | +// @Router /templateversions/{templateversion}/dynamic-parameters [get] |
| 283 | +func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http.Request) { |
| 284 | + ctx := r.Context() |
| 285 | + templateVersion := httpmw.TemplateVersionParam(r) |
| 286 | + |
| 287 | + // Check that the job has completed successfully |
| 288 | + job, err := api.Database.GetProvisionerJobByID(ctx, templateVersion.JobID) |
| 289 | + if err != nil { |
| 290 | + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ |
| 291 | + Message: "Internal error fetching provisioner job.", |
| 292 | + Detail: err.Error(), |
| 293 | + }) |
| 294 | + return |
| 295 | + } |
| 296 | + if !job.CompletedAt.Valid { |
| 297 | + httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ |
| 298 | + Message: "Job hasn't completed!", |
| 299 | + }) |
| 300 | + return |
| 301 | + } |
| 302 | + |
| 303 | + // Having the Terraform plan available for the evaluation engine is helpful |
| 304 | + // for populating values from data blocks, but isn't strictly required. If |
| 305 | + // we don't have a cached plan available, we just use an empty one instead. |
| 306 | + var plan json.RawMessage = []byte("{}") |
| 307 | + tf, err := api.Database.GetTemplateVersionTerraformValues(ctx, templateVersion.ID) |
| 308 | + if err == nil { |
| 309 | + plan = tf.CachedPlan |
| 310 | + } |
| 311 | + |
| 312 | + input := preview.Input{ |
| 313 | + PlanJSON: plan, |
| 314 | + ParameterValues: map[string]string{}, |
| 315 | + Owner: previewtypes.WorkspaceOwner{}, |
| 316 | + } |
| 317 | + |
| 318 | + fileCtx := dbauthz.AsProvisionerd(ctx) |
| 319 | + fileID, err := api.Database.GetFileIDByTemplateVersionID(fileCtx, templateVersion.ID) |
| 320 | + if err != nil { |
| 321 | + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ |
| 322 | + Message: "Internal error finding template version Terraform.", |
| 323 | + Detail: err.Error(), |
| 324 | + }) |
| 325 | + return |
| 326 | + } |
| 327 | + |
| 328 | + fs, err := api.FileCache.Acquire(fileCtx, fileID) |
| 329 | + defer api.FileCache.Release(fileID) |
| 330 | + if err != nil { |
| 331 | + httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ |
| 332 | + Message: "Internal error fetching template version Terraform.", |
| 333 | + Detail: err.Error(), |
| 334 | + }) |
| 335 | + return |
| 336 | + } |
| 337 | + |
| 338 | + conn, err := websocket.Accept(rw, r, nil) |
| 339 | + if err != nil { |
| 340 | + httpapi.Write(ctx, rw, http.StatusUpgradeRequired, codersdk.Response{ |
| 341 | + Message: "Failed to accept WebSocket.", |
| 342 | + Detail: err.Error(), |
| 343 | + }) |
| 344 | + return |
| 345 | + } |
| 346 | + |
| 347 | + stream := wsjson.NewStream[previewweb.Request, previewweb.Response](conn, websocket.MessageText, websocket.MessageText, api.Logger) |
| 348 | + |
| 349 | + // Send an initial form state, computed without any user input. |
| 350 | + result, diagnostics := preview.Preview(ctx, input, fs) |
| 351 | + stream.Send(previewweb.Response{ |
| 352 | + // or maybe it could be -1 or something? it just has to be unique from |
| 353 | + // anything a client could reasonably send. |
| 354 | + ID: math.MaxInt32, |
| 355 | + Parameters: result.Parameters, |
| 356 | + Diagnostics: previewtypes.Diagnostics(diagnostics), |
| 357 | + }) |
| 358 | + |
| 359 | + // As the user types into the form, reprocess the state using their input, |
| 360 | + // and respond with updates. |
| 361 | + updates := stream.Chan() |
| 362 | + for { |
| 363 | + select { |
| 364 | + case <-ctx.Done(): |
| 365 | + return |
| 366 | + case update := <-updates: |
| 367 | + newInput := input |
| 368 | + newInput.ParameterValues = update.Inputs |
| 369 | + result, diagnostics := preview.Preview(ctx, input, fs) |
| 370 | + stream.Send(previewweb.Response{ |
| 371 | + ID: update.ID, |
| 372 | + Parameters: result.Parameters, |
| 373 | + Diagnostics: previewtypes.Diagnostics(diagnostics), |
| 374 | + }) |
| 375 | + } |
| 376 | + } |
| 377 | +} |
| 378 | + |
269 | 379 | // @Summary Get rich parameters by template version
|
270 | 380 | // @ID get-rich-parameters-by-template-version
|
271 | 381 | // @Security CoderSessionToken
|
|
0 commit comments