@@ -37,8 +37,7 @@ import (
37
37
// @Success 101
38
38
// @Router /users/{user}/templateversions/{templateversion}/parameters [get]
39
39
func (api * API ) templateVersionDynamicParameters (rw http.ResponseWriter , r * http.Request ) {
40
- ctx , cancel := context .WithTimeout (r .Context (), 30 * time .Minute )
41
- defer cancel ()
40
+ ctx := r .Context ()
42
41
user := httpmw .UserParam (r )
43
42
templateVersion := httpmw .TemplateVersionParam (r )
44
43
@@ -71,143 +70,45 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
71
70
return
72
71
}
73
72
74
- // staticDiagnostics is a set of diagnostics to be applied to all rendered results.
75
- staticDiagnostics := parameterProvisionerVersionDiagnostic (tf )
76
-
77
- // render is the function that given a set of input values, will return the
78
- // parameter state. There is 2 rendering functions.
79
- //
80
- // prepareStaticPreview uses the static set of parameters saved from the template
81
- // import. These parameters are returned on every request, and have no dynamic
82
- // functionality. This exists for backwards compatibility with older template versions
83
- // which have not uploaded their plan & module files.
84
- //
85
- // prepareDynamicPreview uses the dynamic preview engine.
86
- var render previewFunction
87
73
major , minor , err := apiversion .Parse (tf .ProvisionerdVersion )
88
- if err != nil || major < 1 || (major == 1 && minor < 5 ) {
89
- // Versions < 1.5 do not upload the required files.
90
- // Versions == "" are < 1.5, but we don't know the exact version.
91
- staticRender , err := prepareStaticPreview (ctx , api .Database , templateVersion .ID )
92
- if err != nil {
93
- httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
94
- Message : "Failed to setup static rendering" ,
95
- Detail : err .Error (),
96
- })
97
- return
98
- }
99
- render = staticRender
74
+ // If the api version is not valid or less than 1.5, we need to use the static parameters
75
+ useStaticParams := err != nil || major < 1 || (major == 1 && minor < 5 )
76
+ if useStaticParams {
77
+ api .handleStaticParameters (rw , r , templateVersion .ID )
100
78
} else {
101
- // If the major version is 1.5+, we can use the dynamic preview
102
- dynamicRender , closer , success := prepareDynamicPreview (ctx , rw , api .Database , api .FileCache , tf , templateVersion , user )
103
- if ! success {
104
- return
105
- }
106
- defer closer ()
107
- render = dynamicRender
108
- }
109
-
110
- conn , err := websocket .Accept (rw , r , nil )
111
- if err != nil {
112
- httpapi .Write (ctx , rw , http .StatusUpgradeRequired , codersdk.Response {
113
- Message : "Failed to accept WebSocket." ,
114
- Detail : err .Error (),
115
- })
116
- return
117
- }
118
- stream := wsjson .NewStream [codersdk.DynamicParametersRequest , codersdk.DynamicParametersResponse ](
119
- conn ,
120
- websocket .MessageText ,
121
- websocket .MessageText ,
122
- api .Logger ,
123
- )
124
-
125
- // Send an initial form state, computed without any user input.
126
- result , diagnostics := render (ctx , map [string ]string {})
127
- response := codersdk.DynamicParametersResponse {
128
- ID : - 1 , // Always start with -1.
129
- Diagnostics : previewtypes .Diagnostics (diagnostics .Extend (staticDiagnostics )),
130
- }
131
- if result != nil {
132
- response .Parameters = result .Parameters
133
- }
134
- err = stream .Send (response )
135
- if err != nil {
136
- stream .Drop ()
137
- return
138
- }
139
-
140
- // As the user types into the form, reprocess the state using their input,
141
- // and respond with updates.
142
- updates := stream .Chan ()
143
- for {
144
- select {
145
- case <- ctx .Done ():
146
- stream .Close (websocket .StatusGoingAway )
147
- return
148
- case update , ok := <- updates :
149
- if ! ok {
150
- // The connection has been closed, so there is no one to write to
151
- return
152
- }
153
-
154
- result , diagnostics := render (ctx , update .Inputs )
155
- response := codersdk.DynamicParametersResponse {
156
- ID : update .ID ,
157
- Diagnostics : previewtypes .Diagnostics (diagnostics .Extend (staticDiagnostics )),
158
- }
159
- if result != nil {
160
- response .Parameters = result .Parameters
161
- }
162
- err = stream .Send (response )
163
- if err != nil {
164
- stream .Drop ()
165
- return
166
- }
167
- }
79
+ api .handleDynamicParameters (rw , r , tf , templateVersion )
168
80
}
169
81
}
170
82
171
83
type previewFunction func (ctx context.Context , values map [string ]string ) (* preview.Output , hcl.Diagnostics )
172
84
173
- func prepareDynamicPreview (ctx context.Context , rw http.ResponseWriter , db database.Store , fc * files.Cache , tf database.TemplateVersionTerraformValue , templateVersion database.TemplateVersion , user database.User ) (render previewFunction , closer func (), success bool ) {
174
- // keep track of all files opened
175
- openFiles := make ([]uuid.UUID , 0 )
176
- closeFiles := func () {
177
- for _ , it := range openFiles {
178
- fc .Release (it )
179
- }
180
- }
181
-
182
- // This defer will close the files if the function exits early without success.
183
- // Closing the files is important to avoid having a memory leak.
184
- defer func () {
185
- if ! success {
186
- closeFiles ()
187
- }
188
- }()
85
+ func (api * API ) handleDynamicParameters (rw http.ResponseWriter , r * http.Request , tf database.TemplateVersionTerraformValue , templateVersion database.TemplateVersion ) {
86
+ var (
87
+ ctx = r .Context ()
88
+ user = httpmw .UserParam (r )
89
+ )
189
90
190
91
// nolint:gocritic // We need to fetch the templates files for the Terraform
191
92
// evaluator, and the user likely does not have permission.
192
93
fileCtx := dbauthz .AsProvisionerd (ctx )
193
- fileID , err := db .GetFileIDByTemplateVersionID (fileCtx , templateVersion .ID )
94
+ fileID , err := api . Database .GetFileIDByTemplateVersionID (fileCtx , templateVersion .ID )
194
95
if err != nil {
195
96
httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
196
97
Message : "Internal error finding template version Terraform." ,
197
98
Detail : err .Error (),
198
99
})
199
- return nil , nil , false
100
+ return
200
101
}
201
102
202
103
// Add the file first. Calling `Release` if it fails is a no-op, so this is safe.
203
- openFiles = append ( openFiles , fileID )
204
- templateFS , err := fc . Acquire ( fileCtx , fileID )
104
+ templateFS , err := api . FileCache . Acquire ( fileCtx , fileID )
105
+ defer api . FileCache . Release ( fileID )
205
106
if err != nil {
206
107
httpapi .Write (ctx , rw , http .StatusNotFound , codersdk.Response {
207
108
Message : "Internal error fetching template version Terraform." ,
208
109
Detail : err .Error (),
209
110
})
210
- return nil , nil , false
111
+ return
211
112
}
212
113
213
114
// Having the Terraform plan available for the evaluation engine is helpful
@@ -218,27 +119,27 @@ func prepareDynamicPreview(ctx context.Context, rw http.ResponseWriter, db datab
218
119
plan = tf .CachedPlan
219
120
}
220
121
221
- openFiles = append (openFiles , tf .CachedModuleFiles .UUID )
222
122
if tf .CachedModuleFiles .Valid {
223
- moduleFilesFS , err := fc .Acquire (fileCtx , tf .CachedModuleFiles .UUID )
123
+ moduleFilesFS , err := api .FileCache .Acquire (fileCtx , tf .CachedModuleFiles .UUID )
124
+ defer api .FileCache .Release (fileID )
224
125
if err != nil {
225
126
httpapi .Write (ctx , rw , http .StatusNotFound , codersdk.Response {
226
127
Message : "Internal error fetching Terraform modules." ,
227
128
Detail : err .Error (),
228
129
})
229
- return nil , nil , false
130
+ return
230
131
}
231
132
232
133
templateFS = files .NewOverlayFS (templateFS , []files.Overlay {{Path : ".terraform/modules" , FS : moduleFilesFS }})
233
134
}
234
135
235
- owner , err := getWorkspaceOwnerData (ctx , db , user , templateVersion .OrganizationID )
136
+ owner , err := getWorkspaceOwnerData (ctx , api . Database , user , templateVersion .OrganizationID )
236
137
if err != nil {
237
138
httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
238
139
Message : "Internal error fetching workspace owner." ,
239
140
Detail : err .Error (),
240
141
})
241
- return nil , nil , false
142
+ return
242
143
}
243
144
244
145
input := preview.Input {
@@ -247,18 +148,23 @@ func prepareDynamicPreview(ctx context.Context, rw http.ResponseWriter, db datab
247
148
Owner : owner ,
248
149
}
249
150
250
- return func (ctx context.Context , values map [string ]string ) (* preview.Output , hcl.Diagnostics ) {
151
+ api . handleParameterWebsocket ( rw , r , func (ctx context.Context , values map [string ]string ) (* preview.Output , hcl.Diagnostics ) {
251
152
// Update the input values with the new values.
252
153
// The rest of the input is unchanged.
253
154
input .ParameterValues = values
254
155
return preview .Preview (ctx , input , templateFS )
255
- }, closeFiles , true
156
+ })
256
157
}
257
158
258
- func prepareStaticPreview (ctx context.Context , db database.Store , version uuid.UUID ) (previewFunction , error ) {
259
- dbTemplateVersionParameters , err := db .GetTemplateVersionParameters (ctx , version )
159
+ func (api * API ) handleStaticParameters (rw http.ResponseWriter , r * http.Request , version uuid.UUID ) {
160
+ ctx := r .Context ()
161
+ dbTemplateVersionParameters , err := api .Database .GetTemplateVersionParameters (ctx , version )
260
162
if err != nil {
261
- return nil , xerrors .Errorf ("error fetching template version parameters: %w" , err )
163
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
164
+ Message : "Failed to retrieve template version parameters" ,
165
+ Detail : err .Error (),
166
+ })
167
+ return
262
168
}
263
169
264
170
params := make ([]previewtypes.Parameter , 0 , len (dbTemplateVersionParameters ))
@@ -336,7 +242,7 @@ func prepareStaticPreview(ctx context.Context, db database.Store, version uuid.U
336
242
params = append (params , param )
337
243
}
338
244
339
- return func (_ context.Context , values map [string ]string ) (* preview.Output , hcl.Diagnostics ) {
245
+ api . handleParameterWebsocket ( rw , r , func (ctx context.Context , values map [string ]string ) (* preview.Output , hcl.Diagnostics ) {
340
246
for i := range params {
341
247
param := & params [i ]
342
248
paramValue , ok := values [param .Name ]
@@ -349,9 +255,80 @@ func prepareStaticPreview(ctx context.Context, db database.Store, version uuid.U
349
255
}
350
256
351
257
return & preview.Output {
352
- Parameters : params ,
353
- }, nil
354
- }, nil
258
+ Parameters : params ,
259
+ }, hcl.Diagnostics {
260
+ {
261
+ Severity : hcl .DiagError ,
262
+ Summary : "This template version is missing required metadata to support dynamic parameters. Go back to the classic creation flow." ,
263
+ Detail : "To restore full functionality, please re-import the terraform as a new template version." ,
264
+ },
265
+ }
266
+ })
267
+ }
268
+
269
+ func (api * API ) handleParameterWebsocket (rw http.ResponseWriter , r * http.Request , render previewFunction ) {
270
+ ctx , cancel := context .WithTimeout (r .Context (), 30 * time .Minute )
271
+ defer cancel ()
272
+
273
+ conn , err := websocket .Accept (rw , r , nil )
274
+ if err != nil {
275
+ httpapi .Write (ctx , rw , http .StatusUpgradeRequired , codersdk.Response {
276
+ Message : "Failed to accept WebSocket." ,
277
+ Detail : err .Error (),
278
+ })
279
+ return
280
+ }
281
+ stream := wsjson .NewStream [codersdk.DynamicParametersRequest , codersdk.DynamicParametersResponse ](
282
+ conn ,
283
+ websocket .MessageText ,
284
+ websocket .MessageText ,
285
+ api .Logger ,
286
+ )
287
+
288
+ // Send an initial form state, computed without any user input.
289
+ result , diagnostics := render (ctx , map [string ]string {})
290
+ response := codersdk.DynamicParametersResponse {
291
+ ID : - 1 , // Always start with -1.
292
+ Diagnostics : previewtypes .Diagnostics (diagnostics ),
293
+ }
294
+ if result != nil {
295
+ response .Parameters = result .Parameters
296
+ }
297
+ err = stream .Send (response )
298
+ if err != nil {
299
+ stream .Drop ()
300
+ return
301
+ }
302
+
303
+ // As the user types into the form, reprocess the state using their input,
304
+ // and respond with updates.
305
+ updates := stream .Chan ()
306
+ for {
307
+ select {
308
+ case <- ctx .Done ():
309
+ stream .Close (websocket .StatusGoingAway )
310
+ return
311
+ case update , ok := <- updates :
312
+ if ! ok {
313
+ // The connection has been closed, so there is no one to write to
314
+ return
315
+ }
316
+
317
+ result , diagnostics := render (ctx , update .Inputs )
318
+ response := codersdk.DynamicParametersResponse {
319
+ ID : update .ID ,
320
+ Diagnostics : previewtypes .Diagnostics (diagnostics ),
321
+ }
322
+ if result != nil {
323
+ response .Parameters = result .Parameters
324
+ }
325
+ err = stream .Send (response )
326
+ if err != nil {
327
+ stream .Drop ()
328
+ return
329
+ }
330
+ }
331
+ }
355
332
}
356
333
357
334
func getWorkspaceOwnerData (
@@ -441,31 +418,3 @@ func getWorkspaceOwnerData(
441
418
Groups : groupNames ,
442
419
}, nil
443
420
}
444
-
445
- // parameterProvisionerVersionDiagnostic checks the version of the provisioner
446
- // used to create the template version. If the version is less than 1.5, it
447
- // returns a warning diagnostic. Only versions 1.5+ return the module & plan data
448
- // required.
449
- func parameterProvisionerVersionDiagnostic (tf database.TemplateVersionTerraformValue ) hcl.Diagnostics {
450
- missingMetadata := hcl.Diagnostic {
451
- Severity : hcl .DiagError ,
452
- Summary : "This template version is missing required metadata to support dynamic parameters. Go back to the classic creation flow." ,
453
- Detail : "To restore full functionality, please re-import the terraform as a new template version." ,
454
- }
455
-
456
- if tf .ProvisionerdVersion == "" {
457
- return hcl.Diagnostics {& missingMetadata }
458
- }
459
-
460
- major , minor , err := apiversion .Parse (tf .ProvisionerdVersion )
461
- if err != nil || tf .ProvisionerdVersion == "" {
462
- return hcl.Diagnostics {& missingMetadata }
463
- } else if major < 1 || (major == 1 && minor < 5 ) {
464
- missingMetadata .Detail = "This template version does not support dynamic parameters. " +
465
- "Some options may be missing or incorrect. " +
466
- "Please contact an administrator to update the provisioner and re-import the template version."
467
- return hcl.Diagnostics {& missingMetadata }
468
- }
469
-
470
- return nil
471
- }
0 commit comments