mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
chore: use static params when dynamic param metadata is missing (#17836)
Existing template versions do not have the metadata (modules + plan) in the db. So revert to using static parameter information from the original template import. This data will still be served over the websocket.
This commit is contained in:
@ -18,10 +18,13 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/files"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/wsjson"
|
||||
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
|
||||
"github.com/coder/preview"
|
||||
previewtypes "github.com/coder/preview/types"
|
||||
"github.com/coder/terraform-provider-coder/v2/provider"
|
||||
"github.com/coder/websocket"
|
||||
)
|
||||
|
||||
@ -34,9 +37,7 @@ import (
|
||||
// @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()
|
||||
user := httpmw.UserParam(r)
|
||||
ctx := r.Context()
|
||||
templateVersion := httpmw.TemplateVersionParam(r)
|
||||
|
||||
// Check that the job has completed successfully
|
||||
@ -59,6 +60,33 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
|
||||
return
|
||||
}
|
||||
|
||||
tf, err := api.Database.GetTemplateVersionTerraformValues(ctx, templateVersion.ID)
|
||||
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to retrieve Terraform values for template version",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
major, minor, err := apiversion.Parse(tf.ProvisionerdVersion)
|
||||
// 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 < 6)
|
||||
if useStaticParams {
|
||||
api.handleStaticParameters(rw, r, templateVersion.ID)
|
||||
} else {
|
||||
api.handleDynamicParameters(rw, r, tf, templateVersion)
|
||||
}
|
||||
}
|
||||
|
||||
type previewFunction func(ctx context.Context, values map[string]string) (*preview.Output, hcl.Diagnostics)
|
||||
|
||||
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)
|
||||
@ -71,6 +99,7 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
|
||||
return
|
||||
}
|
||||
|
||||
// Add the file first. Calling `Release` if it fails is a no-op, so this is safe.
|
||||
templateFS, err := api.FileCache.Acquire(fileCtx, fileID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||
@ -85,34 +114,25 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
|
||||
// for populating values from data blocks, but isn't strictly required. If
|
||||
// we don't have a cached plan available, we just use an empty one instead.
|
||||
plan := json.RawMessage("{}")
|
||||
tf, err := api.Database.GetTemplateVersionTerraformValues(ctx, templateVersion.ID)
|
||||
if err == nil {
|
||||
if len(tf.CachedPlan) > 0 {
|
||||
plan = tf.CachedPlan
|
||||
|
||||
if tf.CachedModuleFiles.Valid {
|
||||
moduleFilesFS, err := api.FileCache.Acquire(fileCtx, tf.CachedModuleFiles.UUID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: "Internal error fetching Terraform modules.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
defer api.FileCache.Release(tf.CachedModuleFiles.UUID)
|
||||
templateFS = files.NewOverlayFS(templateFS, []files.Overlay{{Path: ".terraform/modules", FS: moduleFilesFS}})
|
||||
}
|
||||
} else if !xerrors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to retrieve Terraform values for template version",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// If the err is sql.ErrNoRows, an empty terraform values struct is correct.
|
||||
staticDiagnostics := parameterProvisionerVersionDiagnostic(tf)
|
||||
if tf.CachedModuleFiles.Valid {
|
||||
moduleFilesFS, err := api.FileCache.Acquire(fileCtx, tf.CachedModuleFiles.UUID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: "Internal error fetching Terraform modules.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
defer api.FileCache.Release(tf.CachedModuleFiles.UUID)
|
||||
|
||||
owner, err := api.getWorkspaceOwnerData(ctx, user, templateVersion.OrganizationID)
|
||||
templateFS = files.NewOverlayFS(templateFS, []files.Overlay{{Path: ".terraform/modules", FS: moduleFilesFS}})
|
||||
}
|
||||
|
||||
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.",
|
||||
@ -127,6 +147,129 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
|
||||
Owner: owner,
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
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))
|
||||
for _, it := range dbTemplateVersionParameters {
|
||||
param := previewtypes.Parameter{
|
||||
ParameterData: previewtypes.ParameterData{
|
||||
Name: it.Name,
|
||||
DisplayName: it.DisplayName,
|
||||
Description: it.Description,
|
||||
Type: previewtypes.ParameterType(it.Type),
|
||||
FormType: "", // ooooof
|
||||
Styling: previewtypes.ParameterStyling{},
|
||||
Mutable: it.Mutable,
|
||||
DefaultValue: previewtypes.StringLiteral(it.DefaultValue),
|
||||
Icon: it.Icon,
|
||||
Options: make([]*previewtypes.ParameterOption, 0),
|
||||
Validations: make([]*previewtypes.ParameterValidation, 0),
|
||||
Required: it.Required,
|
||||
Order: int64(it.DisplayOrder),
|
||||
Ephemeral: it.Ephemeral,
|
||||
Source: nil,
|
||||
},
|
||||
// Always use the default, since we used to assume the empty string
|
||||
Value: previewtypes.StringLiteral(it.DefaultValue),
|
||||
Diagnostics: nil,
|
||||
}
|
||||
|
||||
if it.ValidationError != "" || it.ValidationRegex != "" || it.ValidationMonotonic != "" {
|
||||
var reg *string
|
||||
if it.ValidationRegex != "" {
|
||||
reg = ptr.Ref(it.ValidationRegex)
|
||||
}
|
||||
|
||||
var vMin *int64
|
||||
if it.ValidationMin.Valid {
|
||||
vMin = ptr.Ref(int64(it.ValidationMin.Int32))
|
||||
}
|
||||
|
||||
var vMax *int64
|
||||
if it.ValidationMax.Valid {
|
||||
vMin = ptr.Ref(int64(it.ValidationMax.Int32))
|
||||
}
|
||||
|
||||
var monotonic *string
|
||||
if it.ValidationMonotonic != "" {
|
||||
monotonic = ptr.Ref(it.ValidationMonotonic)
|
||||
}
|
||||
|
||||
param.Validations = append(param.Validations, &previewtypes.ParameterValidation{
|
||||
Error: it.ValidationError,
|
||||
Regex: reg,
|
||||
Min: vMin,
|
||||
Max: vMax,
|
||||
Monotonic: monotonic,
|
||||
})
|
||||
}
|
||||
|
||||
var protoOptions []*sdkproto.RichParameterOption
|
||||
_ = json.Unmarshal(it.Options, &protoOptions) // Not going to make this fatal
|
||||
for _, opt := range protoOptions {
|
||||
param.Options = append(param.Options, &previewtypes.ParameterOption{
|
||||
Name: opt.Name,
|
||||
Description: opt.Description,
|
||||
Value: previewtypes.StringLiteral(opt.Value),
|
||||
Icon: opt.Icon,
|
||||
})
|
||||
}
|
||||
|
||||
// Take the form type from the ValidateFormType function. This is a bit
|
||||
// unfortunate we have to do this, but it will return the default form_type
|
||||
// for a given set of conditions.
|
||||
_, param.FormType, _ = provider.ValidateFormType(provider.OptionType(param.Type), len(param.Options), param.FormType)
|
||||
|
||||
param.Diagnostics = previewtypes.Diagnostics(param.Valid(param.Value))
|
||||
params = append(params, param)
|
||||
}
|
||||
|
||||
api.handleParameterWebsocket(rw, r, func(_ context.Context, values map[string]string) (*preview.Output, hcl.Diagnostics) {
|
||||
for i := range params {
|
||||
param := ¶ms[i]
|
||||
paramValue, ok := values[param.Name]
|
||||
if ok {
|
||||
param.Value = previewtypes.StringLiteral(paramValue)
|
||||
} else {
|
||||
param.Value = param.DefaultValue
|
||||
}
|
||||
param.Diagnostics = previewtypes.Diagnostics(param.Valid(param.Value))
|
||||
}
|
||||
|
||||
return &preview.Output{
|
||||
Parameters: params,
|
||||
}, hcl.Diagnostics{
|
||||
{
|
||||
// Only a warning because the form does still work.
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "This template version is missing required metadata to support dynamic parameters.",
|
||||
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) {
|
||||
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{
|
||||
@ -143,10 +286,10 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
|
||||
)
|
||||
|
||||
// Send an initial form state, computed without any user input.
|
||||
result, diagnostics := preview.Preview(ctx, input, templateFS)
|
||||
result, diagnostics := render(ctx, map[string]string{})
|
||||
response := codersdk.DynamicParametersResponse{
|
||||
ID: -1,
|
||||
Diagnostics: previewtypes.Diagnostics(diagnostics.Extend(staticDiagnostics)),
|
||||
ID: -1, // Always start with -1.
|
||||
Diagnostics: previewtypes.Diagnostics(diagnostics),
|
||||
}
|
||||
if result != nil {
|
||||
response.Parameters = result.Parameters
|
||||
@ -170,11 +313,11 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
|
||||
// The connection has been closed, so there is no one to write to
|
||||
return
|
||||
}
|
||||
input.ParameterValues = update.Inputs
|
||||
result, diagnostics := preview.Preview(ctx, input, templateFS)
|
||||
|
||||
result, diagnostics := render(ctx, update.Inputs)
|
||||
response := codersdk.DynamicParametersResponse{
|
||||
ID: update.ID,
|
||||
Diagnostics: previewtypes.Diagnostics(diagnostics.Extend(staticDiagnostics)),
|
||||
Diagnostics: previewtypes.Diagnostics(diagnostics),
|
||||
}
|
||||
if result != nil {
|
||||
response.Parameters = result.Parameters
|
||||
@ -188,8 +331,9 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) getWorkspaceOwnerData(
|
||||
func getWorkspaceOwnerData(
|
||||
ctx context.Context,
|
||||
db database.Store,
|
||||
user database.User,
|
||||
organizationID uuid.UUID,
|
||||
) (previewtypes.WorkspaceOwner, error) {
|
||||
@ -200,7 +344,7 @@ func (api *API) getWorkspaceOwnerData(
|
||||
// nolint:gocritic // This is kind of the wrong query to use here, but it
|
||||
// matches how the provisioner currently works. We should figure out
|
||||
// something that needs less escalation but has the correct behavior.
|
||||
row, err := api.Database.GetAuthorizationUserRoles(dbauthz.AsSystemRestricted(ctx), user.ID)
|
||||
row, err := db.GetAuthorizationUserRoles(dbauthz.AsSystemRestricted(ctx), user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -227,7 +371,10 @@ func (api *API) getWorkspaceOwnerData(
|
||||
|
||||
var publicKey string
|
||||
g.Go(func() error {
|
||||
key, err := api.Database.GetGitSSHKey(ctx, user.ID)
|
||||
// The correct public key has to be sent. This will not be leaked
|
||||
// unless the template leaks it.
|
||||
// nolint:gocritic
|
||||
key, err := db.GetGitSSHKey(dbauthz.AsSystemRestricted(ctx), user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -237,7 +384,11 @@ func (api *API) getWorkspaceOwnerData(
|
||||
|
||||
var groupNames []string
|
||||
g.Go(func() error {
|
||||
groups, err := api.Database.GetGroups(ctx, database.GetGroupsParams{
|
||||
// The groups need to be sent to preview. These groups are not exposed to the
|
||||
// user, unless the template does it through the parameters. Regardless, we need
|
||||
// the correct groups, and a user might not have read access.
|
||||
// nolint:gocritic
|
||||
groups, err := db.GetGroups(dbauthz.AsSystemRestricted(ctx), database.GetGroupsParams{
|
||||
OrganizationID: organizationID,
|
||||
HasMemberID: user.ID,
|
||||
})
|
||||
@ -267,31 +418,3 @@ func (api *API) getWorkspaceOwnerData(
|
||||
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
|
||||
}
|
||||
|
Reference in New Issue
Block a user