8000 chore: refactor websocket code into own function · coder/coder@4dd4149 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4dd4149

Browse files
committed
chore: refactor websocket code into own function
Dynamic & static code flow is now independent
1 parent 1375619 commit 4dd4149

File tree

2 files changed

+106
-234
lines changed

2 files changed

+106
-234
lines changed

coderd/parameters.go

Lines changed: 106 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ import (
3737
// @Success 101
3838
// @Router /users/{user}/templateversions/{templateversion}/parameters [get]
3939
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()
4241
user := httpmw.UserParam(r)
4342
templateVersion := httpmw.TemplateVersionParam(r)
4443

@@ -71,143 +70,45 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
7170
return
7271
}
7372

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
8773
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)
10078
} 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)
16880
}
16981
}
17082

17183
type previewFunction func(ctx context.Context, values map[string]string) (*preview.Output, hcl.Diagnostics)
17284

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+
)
18990

19091
// nolint:gocritic // We need to fetch the templates files for the Terraform
19192
// evaluator, and the user likely does not have permission.
19293
fileCtx := dbauthz.AsProvisionerd(ctx)
193-
fileID, err := db.GetFileIDByTemplateVersionID(fileCtx, templateVersion.ID)
94+
fileID, err := api.Database.GetFileIDByTemplateVersionID(fileCtx, templateVersion.ID)
19495
if err != nil {
19596
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
19697
Message: "Internal error finding template version Terraform.",
19798
Detail: err.Error(),
19899
})
199-
return nil, nil, false
100+
return
200101
}
201102

202103
// 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)
205106
if err != nil {
206107
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
207108
Message: "Internal error fetching template version Terraform.",
208109
Detail: err.Error(),
209110
})
210-
return nil, nil, false
111+
return
211112
}
212113

213114
// 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
218119
plan = tf.CachedPlan
219120
}
220121

221-
openFiles = append(openFiles, tf.CachedModuleFiles.UUID)
222122
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)
224125
if err != nil {
225126
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
226127
Message: "Internal error fetching Terraform modules.",
227128
Detail: err.Error(),
228129
})
229-
return nil, nil, false
130+
return
230131
}
231132

232133
templateFS = files.NewOverlayFS(templateFS, []files.Overlay{{Path: ".terraform/modules", FS: moduleFilesFS}})
233134
}
234135

235-
owner, err := getWorkspaceOwnerData(ctx, db, user, templateVersion.OrganizationID)
136+
owner, err := getWorkspaceOwnerData(ctx, api.Database, user, templateVersion.OrganizationID)
236137
if err != nil {
237138
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
238139
Message: "Internal error fetching workspace owner.",
239140
Detail: err.Error(),
240141
})
241-
return nil, nil, false
142+
return
242143
}
243144

244145
input := preview.Input{
@@ -247,18 +148,23 @@ func prepareDynamicPreview(ctx context.Context, rw http.ResponseWriter, db datab
247148
Owner: owner,
248149
}
249150

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) {
251152
// Update the input values with the new values.
252153
// The rest of the input is unchanged.
253154
input.ParameterValues = values
254155
return preview.Preview(ctx, input, templateFS)
255-
}, closeFiles, true
156+
})
256157
}
257158

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)
260162
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
262168
}
263169

264170
params := make([]previewtypes.Parameter, 0, len(dbTemplateVersionParameters))
@@ -336,7 +242,7 @@ func prepareStaticPreview(ctx context.Context, db database.Store, version uuid.U
336242
params = append(params, param)
337243
}
338244

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) {
340246
for i := range params {
341247
param := &params[i]
342248
paramValue, ok := values[param.Name]
@@ -349,9 +255,80 @@ func prepareStaticPreview(ctx context.Context, db database.Store, version uuid.U
349255
}
350256

351257
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+
}
355332
}
356333

357334
func getWorkspaceOwnerData(
@@ -441,31 +418,3 @@ func getWorkspaceOwnerData(
441418
Groups: groupNames,
442419
}, nil
443420
}
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

Comments
 (0)
0