-
Notifications
You must be signed in to change notification settings - Fork 899
chore: use static params when dynamic param metadata is missing #17836
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
34de4f0
chore: static params as dynmaic
Emyrk 005a084
chore: refactor preview code to be swappable
Emyrk 55d2af4
Merge remote-tracking branch 'origin/main' into stevenmasley/use_stat…
Emyrk 2f5f251
wip
Emyrk d9c5ce5
Working prototype
Emyrk 6299f43
work on unit testing
Emyrk f80d838
chore: add unit test for static values
Emyrk a5ee374
revert changes to cache
Emyrk 3cb39ea
test: add unit test for closing files
Emyrk d5a68dd
linting
Emyrk bbd0803
chore: move group test to enterprise
Emyrk f5fe4b7
linting
Emyrk dfd0e24
linting
Emyrk 1375619
revert deleted coderd/parameters_internal_test.go
Emyrk 4dd4149
chore: refactor websocket code into own function
Emyrk f3cff03
fixup
Emyrk 13215d6
fix leaked files
Emyrk 72f2234
fix up the error message for static params for now
Emyrk 4ea0165
linting
Emyrk ef15df1
move release to only if err == nil
Emyrk df65b92
use options pattern for provisioner version override
Emyrk 2bb3749
Merge remote-tracking branch 'origin/main' into stevenmasley/use_stat…
Emyrk 5957960
fixup error message assert
Emyrk 62db9dd
1.5 -> 1.6
Emyrk 5a9b4ac
Merge remote-tracking branch 'origin/main' into stevenmasley/use_stat…
Emyrk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
chore: refactor websocket code into own function
Dynamic & static code flow is now independent
- Loading branch information
commit 4dd41495618b44c6de5422eeba5f8b0f58750ae5
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -37,9 +37,8 @@ | |||||
// @Success 101 | ||||||
// @Router /users/{user}/templateversions/{templateversion}/parameters [get] | ||||||
func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http.Request) { | ||||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Minute) | ||||||
defer cancel() | ||||||
ctx := r.Context() | ||||||
user := httpmw.UserParam(r) | ||||||
templateVersion := httpmw.TemplateVersionParam(r) | ||||||
|
||||||
// Check that the job has completed successfully | ||||||
|
@@ -71,143 +70,45 @@ | |||||
return | ||||||
} | ||||||
|
||||||
// staticDiagnostics is a set of diagnostics to be applied to all rendered results. | ||||||
staticDiagnostics := parameterProvisionerVersionDiagnostic(tf) | ||||||
|
||||||
// render is the function that given a set of input values, will return the | ||||||
// parameter state. There is 2 rendering functions. | ||||||
// | ||||||
// prepareStaticPreview uses the static set of parameters saved from the template | ||||||
// import. These parameters are returned on every request, and have no dynamic | ||||||
// functionality. This exists for backwards compatibility with older template versions | ||||||
// which have not uploaded their plan & module files. | ||||||
// | ||||||
// prepareDynamicPreview uses the dynamic preview engine. | ||||||
var render previewFunction | ||||||
major, minor, err := apiversion.Parse(tf.ProvisionerdVersion) | ||||||
if err != nil || major < 1 || (major == 1 && minor < 5) { | ||||||
// Versions < 1.5 do not upload the required files. | ||||||
// Versions == "" are < 1.5, but we don't know the exact version. | ||||||
staticRender, err := prepareStaticPreview(ctx, api.Database, templateVersion.ID) | ||||||
if err != nil { | ||||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ | ||||||
Message: "Failed to setup static rendering", | ||||||
Detail: err.Error(), | ||||||
}) | ||||||
return | ||||||
} | ||||||
render = staticRender | ||||||
// If the api version is not valid or less than 1.5, we need to use the static parameters | ||||||
useStaticParams := err != nil || major < 1 || (major == 1 && minor < 5) | ||||||
if useStaticParams { | ||||||
api.handleStaticParameters(rw, r, templateVersion.ID) | ||||||
} else { | ||||||
// If the major version is 1.5+, we can use the dynamic preview | ||||||
dynamicRender, closer, success := prepareDynamicPreview(ctx, rw, api.Database, api.FileCache, tf, templateVersion, user) | ||||||
if !success { | ||||||
return | ||||||
} | ||||||
defer closer() | ||||||
render = dynamicRender | ||||||
} | ||||||
|
||||||
conn, err := websocket.Accept(rw, r, nil) | ||||||
if err != nil { | ||||||
httpapi.Write(ctx, rw, http.StatusUpgradeRequired, codersdk.Response{ | ||||||
Message: "Failed to accept WebSocket.", | ||||||
Detail: err.Error(), | ||||||
}) | ||||||
return | ||||||
} | ||||||
stream := wsjson.NewStream[codersdk.DynamicParametersRequest, codersdk.DynamicParametersResponse]( | ||||||
conn, | ||||||
websocket.MessageText, | ||||||
websocket.MessageText, | ||||||
api.Logger, | ||||||
) | ||||||
|
||||||
// Send an initial form state, computed without any user input. | ||||||
result, diagnostics := render(ctx, map[string]string{}) | ||||||
response := codersdk.DynamicParametersResponse{ | ||||||
ID: -1, // Always start with -1. | ||||||
Diagnostics: previewtypes.Diagnostics(diagnostics.Extend(staticDiagnostics)), | ||||||
} | ||||||
if result != nil { | ||||||
response.Parameters = result.Parameters | ||||||
} | ||||||
err = stream.Send(response) | ||||||
if err != nil { | ||||||
stream.Drop() | ||||||
return | ||||||
} | ||||||
|
||||||
// As the user types into the form, reprocess the state using their input, | ||||||
// and respond with updates. | ||||||
updates := stream.Chan() | ||||||
for { | ||||||
select { | ||||||
case <-ctx.Done(): | ||||||
stream.Close(websocket.StatusGoingAway) | ||||||
return | ||||||
case update, ok := <-updates: | ||||||
if !ok { | ||||||
// The connection has been closed, so there is no one to write to | ||||||
return | ||||||
} | ||||||
|
||||||
result, diagnostics := render(ctx, update.Inputs) | ||||||
response := codersdk.DynamicParametersResponse{ | ||||||
ID: update.ID, | ||||||
Diagnostics: previewtypes.Diagnostics(diagnostics.Extend(staticDiagnostics)), | ||||||
} | ||||||
if result != nil { | ||||||
response.Parameters = result.Parameters | ||||||
} | ||||||
err = stream.Send(response) | ||||||
if err != nil { | ||||||
stream.Drop() | ||||||
return | ||||||
} | ||||||
} | ||||||
api.handleDynamicParameters(rw, r, tf, templateVersion) | ||||||
} | ||||||
} | ||||||
|
||||||
type previewFunction func(ctx context.Context, values map[string]string) (*preview.Output, hcl.Diagnostics) | ||||||
|
||||||
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) { | ||||||
// keep track of all files opened | ||||||
openFiles := make([]uuid.UUID, 0) | ||||||
closeFiles := func() { | ||||||
for _, it := range openFiles { | ||||||
fc.Release(it) | ||||||
} | ||||||
} | ||||||
|
||||||
// This defer will close the files if the function exits early without success. | ||||||
// Closing the files is important to avoid having a memory leak. | ||||||
defer func() { | ||||||
if !success { | ||||||
closeFiles() | ||||||
} | ||||||
}() | ||||||
func (api *API) handleDynamicParameters(rw http.ResponseWriter, r *http.Request, tf database.TemplateVersionTerraformValue, templateVersion database.TemplateVersion) { | ||||||
var ( | ||||||
ctx = r.Context() | ||||||
user = httpmw.UserParam(r) | ||||||
) | ||||||
|
||||||
// nolint:gocritic // We need to fetch the templates files for the Terraform | ||||||
// evaluator, and the user likely does not have permission. | ||||||
fileCtx := dbauthz.AsProvisionerd(ctx) | ||||||
fileID, err := db.GetFileIDByTemplateVersionID(fileCtx, templateVersion.ID) | ||||||
fileID, err := api.Database.GetFileIDByTemplateVersionID(fileCtx, templateVersion.ID) | ||||||
if err != nil { | ||||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ | ||||||
Message: "Internal error finding template version Terraform.", | ||||||
Detail: err.Error(), | ||||||
}) | ||||||
return nil, nil, false | ||||||
return | ||||||
} | ||||||
|
||||||
// Add the file first. Calling `Release` if it fails is a no-op, so this is safe. | ||||||
openFiles = append(openFiles, fileID) | ||||||
templateFS, err := fc.Acquire(fileCtx, fileID) | ||||||
templateFS, err := api.FileCache.Acquire(fileCtx, fileID) | ||||||
defer api.FileCache.Release(fileID) | ||||||
Emyrk marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
if err != nil { | ||||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ | ||||||
Message: "Internal error fetching template version Terraform.", | ||||||
Detail: err.Error(), | ||||||
}) | ||||||
return nil, nil, false | ||||||
return | ||||||
} | ||||||
|
||||||
// Having the Terraform plan available for the evaluation engine is helpful | ||||||
|
@@ -218,27 +119,27 @@ | |||||
plan = tf.CachedPlan | ||||||
} | ||||||
|
||||||
openFiles = append(openFiles, tf.CachedModuleFiles.UUID) | ||||||
if tf.CachedModuleFiles.Valid { | ||||||
moduleFilesFS, err := fc.Acquire(fileCtx, tf.CachedModuleFiles.UUID) | ||||||
moduleFilesFS, err := api.FileCache.Acquire(fileCtx, tf.CachedModuleFiles.UUID) | ||||||
defer api.FileCache.Release(fileID) | ||||||
if err != nil { | ||||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ | ||||||
Message: "Internal error fetching Terraform modules.", | ||||||
Detail: err.Error(), | ||||||
}) | ||||||
return nil, nil, false | ||||||
return | ||||||
} | ||||||
|
||||||
templateFS = files.NewOverlayFS(templateFS, []files.Overlay{{Path: ".terraform/modules", FS: moduleFilesFS}}) | ||||||
} | ||||||
|
||||||
owner, err := getWorkspaceOwnerData(ctx, db, user, templateVersion.OrganizationID) | ||||||
owner, err := getWorkspaceOwnerData(ctx, api.Database, user, templateVersion.OrganizationID) | ||||||
if err != nil { | ||||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ | ||||||
Message: "Internal error fetching workspace owner.", | ||||||
Detail: err.Error(), | ||||||
}) | ||||||
return nil, nil, false | ||||||
return | ||||||
} | ||||||
|
||||||
input := preview.Input{ | ||||||
|
@@ -247,18 +148,23 @@ | |||||
Owner: owner, | ||||||
} | ||||||
|
||||||
return func(ctx context.Context, values map[string]string) (*preview.Output, hcl.Diagnostics) { | ||||||
api.handleParameterWebsocket(rw, r, func(ctx context.Context, values map[string]string) (*preview.Output, hcl.Diagnostics) { | ||||||
// Update the input values with the new values. | ||||||
// The rest of the input is unchanged. | ||||||
input.ParameterValues = values | ||||||
return preview.Preview(ctx, input, templateFS) | ||||||
}, closeFiles, true | ||||||
}) | ||||||
} | ||||||
|
||||||
func prepareStaticPreview(ctx context.Context, db database.Store, version uuid.UUID) (previewFunction, error) { | ||||||
dbTemplateVersionParameters, err := db.GetTemplateVersionParameters(ctx, version) | ||||||
func (api *API) handleStaticParameters(rw http.ResponseWriter, r *http.Request, version uuid.UUID) { | ||||||
ctx := r.Context() | ||||||
dbTemplateVersionParameters, err := api.Database.GetTemplateVersionParameters(ctx, version) | ||||||
if err != nil { | ||||||
return nil, xerrors.Errorf("error fetching template version parameters: %w", err) | ||||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ | ||||||
Message: "Failed to retrieve template version parameters", | ||||||
Detail: err.Error(), | ||||||
}) | ||||||
return | ||||||
} | ||||||
|
||||||
params := make([]previewtypes.Parameter, 0, len(dbTemplateVersionParameters)) | ||||||
|
@@ -336,7 +242,7 @@ | |||||
params = append(params, param) | ||||||
} | ||||||
|
||||||
return func(_ context.Context, values map[string]string) (*preview.Output, hcl.Diagnostics) { | ||||||
api.handleParameterWebsocket(rw, r, func(ctx context.Context, values map[string]string) (*preview.Output, hcl.Diagnostics) { | ||||||
for i := range params { | ||||||
param := ¶ms[i] | ||||||
paramValue, ok := values[param.Name] | ||||||
|
@@ -349,9 +255,80 @@ | |||||
} | ||||||
|
||||||
return &preview.Output{ | ||||||
Parameters: params, | ||||||
}, nil | ||||||
}, nil | ||||||
Parameters: params, | ||||||
}, hcl.Diagnostics{ | ||||||
{ | ||||||
Severity: hcl.DiagError, | ||||||
Summary: "This template version is missing required metadata to support dynamic parameters. Go back to the classic creation flow.", | ||||||
Detail: "To restore full functionality, please re-import the terraform as a new template version.", | ||||||
}, | ||||||
} | ||||||
}) | ||||||
} | ||||||
|
||||||
func (api *API) handleParameterWebsocket(rw http.ResponseWriter, r *http.Request, render previewFunction) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
still think we should call it preview, especially since that's what you named the type There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will collide with the package name 😢 |
||||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Minute) | ||||||
defer cancel() | ||||||
|
||||||
conn, err := websocket.Accept(rw, r, nil) | ||||||
if err != nil { | ||||||
httpapi.Write(ctx, rw, http.StatusUpgradeRequired, codersdk.Response{ | ||||||
Message: "Failed to accept WebSocket.", | ||||||
Detail: err.Error(), | ||||||
}) | ||||||
return | ||||||
} | ||||||
stream := wsjson.NewStream[codersdk.DynamicParametersRequest, codersdk.DynamicParametersResponse]( | ||||||
conn, | ||||||
websocket.MessageText, | ||||||
websocket.MessageText, | ||||||
api.Logger, | ||||||
) | ||||||
|
||||||
// Send an initial form state, computed without any user input. | ||||||
result, diagnostics := render(ctx, map[string]string{}) | ||||||
response := codersdk.DynamicParametersResponse{ | ||||||
ID: -1, // Always start with -1. | ||||||
Diagnostics: previewtypes.Diagnostics(diagnostics), | ||||||
} | ||||||
if result != nil { | ||||||
response.Parameters = result.Parameters | ||||||
} | ||||||
err = stream.Send(response) | ||||||
if err != nil { | ||||||
stream.Drop() | ||||||
return | ||||||
} | ||||||
|
||||||
// As the user types into the form, reprocess the state using their input, | ||||||
// and respond with updates. | ||||||
updates := stream.Chan() | ||||||
for { | ||||||
select { | ||||||
case <-ctx.Done(): | ||||||
stream.Close(websocket.StatusGoingAway) | ||||||
return | ||||||
case update, ok := <-updates: | ||||||
if !ok { | ||||||
// The connection has been closed, so there is no one to write to | ||||||
return | ||||||
} | ||||||
|
||||||
result, diagnostics := render(ctx, update.Inputs) | ||||||
response := codersdk.DynamicParametersResponse{ | ||||||
ID: update.ID, | ||||||
Diagnostics: previewtypes.Diagnostics(diagnostics), | ||||||
} | ||||||
if result != nil { | ||||||
response.Parameters = result.Parameters | ||||||
} | ||||||
err = stream.Send(response) | ||||||
if err != nil { | ||||||
stream.Drop() | ||||||
return | ||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
func getWorkspaceOwnerData( | ||||||
|
@@ -441,31 +418,3 @@ | |||||
Groups: groupNames, | ||||||
}, nil | ||||||
} | ||||||
|
||||||
// parameterProvisionerVersionDiagnostic checks the version of the provisioner | ||||||
// used to create the template version. If the version is less than 1.5, it | ||||||
// returns a warning diagnostic. Only versions 1.5+ return the module & plan data | ||||||
// required. | ||||||
func parameterProvisionerVersionDiagnostic(tf database.TemplateVersionTerraformValue) hcl.Diagnostics { | ||||||
missingMetadata := hcl.Diagnostic{ | ||||||
Severity: hcl.DiagError, | ||||||
Summary: "This template version is missing required metadata to support dynamic parameters. Go back to the classic creation flow.", | ||||||
Detail: "To restore full functionality, please re-import the terraform as a new template version.", | ||||||
} | ||||||
|
||||||
if tf.ProvisionerdVersion == "" { | ||||||
return hcl.Diagnostics{&missingMetadata} | ||||||
} | ||||||
|
||||||
major, minor, err := apiversion.Parse(tf.ProvisionerdVersion) | ||||||
if err != nil || tf.ProvisionerdVersion == "" { | ||||||
return hcl.Diagnostics{&missingMetadata} | ||||||
} else if major < 1 || (major == 1 && minor < 5) { | ||||||
missingMetadata.Detail = "This template version does not support dynamic parameters. " + | ||||||
"Some options may be missing or incorrect. " + | ||||||
"Please contact an administrator to update the provisioner and re-import the template version." | ||||||
return hcl.Diagnostics{&missingMetadata} | ||||||
} | ||||||
|
||||||
return nil | ||||||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.