|
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 | +iferr!=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 | +varplan json.RawMessage= []byte("{}") |
| 307 | +tf,err:=api.Database.GetTemplateVersionTerraformValues(ctx,templateVersion.ID) |
| 308 | +iferr==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 | +iferr!=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 | +deferapi.FileCache.Release(fileID) |
| 330 | +iferr!=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 | +iferr!=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 | +caseupdate:=<-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
|
|