feat!: Validate monotonic numbers for rich parameters (#6046)

* Database changes

* protobuf

* Fix: docs

* workspaces_test

* Validation in coderd

* Fix: resources

* omitempty

* UI changes

* UI tests

* fix
This commit is contained in:
Marcin Tojek
2023-02-07 09:36:13 +01:00
committed by GitHub
parent e3ae664a29
commit b86bce8494
32 changed files with 917 additions and 392 deletions
+86 -1
View File
@@ -2636,7 +2636,7 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/parameter.ComputedValue"
"$ref": "#/definitions/codersdk.TemplateVersionParameter"
}
}
}
@@ -7543,6 +7543,80 @@ const docTemplate = `{
}
}
},
"codersdk.TemplateVersionParameter": {
"type": "object",
"properties": {
"default_value": {
"type": "string"
},
"description": {
"type": "string"
},
"icon": {
"type": "string"
},
"mutable": {
"type": "boolean"
},
"name": {
"type": "string"
},
"options": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.TemplateVersionParameterOption"
}
},
"type": {
"type": "string",
"enum": [
"string",
"number",
"bool"
]
},
"validation_error": {
"type": "string"
},
"validation_max": {
"type": "integer"
},
"validation_min": {
"type": "integer"
},
"validation_monotonic": {
"enum": [
"increasing",
"decreasing"
],
"allOf": [
{
"$ref": "#/definitions/codersdk.ValidationMonotonicOrder"
}
]
},
"validation_regex": {
"type": "string"
}
}
},
"codersdk.TemplateVersionParameterOption": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"icon": {
"type": "string"
},
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"codersdk.TraceConfig": {
"type": "object",
"properties": {
@@ -7769,6 +7843,17 @@ const docTemplate = `{
}
}
},
"codersdk.ValidationMonotonicOrder": {
"type": "string",
"enum": [
"increasing",
"decreasing"
],
"x-enum-varnames": [
"MonotonicOrderIncreasing",
"MonotonicOrderDecreasing"
]
},
"codersdk.Workspace": {
"type": "object",
"properties": {
+76 -1
View File
@@ -2320,7 +2320,7 @@
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/parameter.ComputedValue"
"$ref": "#/definitions/codersdk.TemplateVersionParameter"
}
}
}
@@ -6781,6 +6781,73 @@
}
}
},
"codersdk.TemplateVersionParameter": {
"type": "object",
"properties": {
"default_value": {
"type": "string"
},
"description": {
"type": "string"
},
"icon": {
"type": "string"
},
"mutable": {
"type": "boolean"
},
"name": {
"type": "string"
},
"options": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.TemplateVersionParameterOption"
}
},
"type": {
"type": "string",
"enum": ["string", "number", "bool"]
},
"validation_error": {
"type": "string"
},
"validation_max": {
"type": "integer"
},
"validation_min": {
"type": "integer"
},
"validation_monotonic": {
"enum": ["increasing", "decreasing"],
"allOf": [
{
"$ref": "#/definitions/codersdk.ValidationMonotonicOrder"
}
]
},
"validation_regex": {
"type": "string"
}
}
},
"codersdk.TemplateVersionParameterOption": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"icon": {
"type": "string"
},
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"codersdk.TraceConfig": {
"type": "object",
"properties": {
@@ -6984,6 +7051,14 @@
}
}
},
"codersdk.ValidationMonotonicOrder": {
"type": "string",
"enum": ["increasing", "decreasing"],
"x-enum-varnames": [
"MonotonicOrderIncreasing",
"MonotonicOrderDecreasing"
]
},
"codersdk.Workspace": {
"type": "object",
"properties": {
+13 -12
View File
@@ -2573,18 +2573,19 @@ func (q *fakeQuerier) InsertTemplateVersionParameter(_ context.Context, arg data
//nolint:gosimple
param := database.TemplateVersionParameter{
TemplateVersionID: arg.TemplateVersionID,
Name: arg.Name,
Description: arg.Description,
Type: arg.Type,
Mutable: arg.Mutable,
DefaultValue: arg.DefaultValue,
Icon: arg.Icon,
Options: arg.Options,
ValidationError: arg.ValidationError,
ValidationRegex: arg.ValidationRegex,
ValidationMin: arg.ValidationMin,
ValidationMax: arg.ValidationMax,
TemplateVersionID: arg.TemplateVersionID,
Name: arg.Name,
Description: arg.Description,
Type: arg.Type,
Mutable: arg.Mutable,
DefaultValue: arg.DefaultValue,
Icon: arg.Icon,
Options: arg.Options,
ValidationError: arg.ValidationError,
ValidationRegex: arg.ValidationRegex,
ValidationMin: arg.ValidationMin,
ValidationMax: arg.ValidationMax,
ValidationMonotonic: arg.ValidationMonotonic,
}
q.templateVersionParameters = append(q.templateVersionParameters, param)
return param, nil
+5 -1
View File
@@ -353,7 +353,9 @@ CREATE TABLE template_version_parameters (
validation_regex text NOT NULL,
validation_min integer NOT NULL,
validation_max integer NOT NULL,
validation_error text DEFAULT ''::text NOT NULL
validation_error text DEFAULT ''::text NOT NULL,
validation_monotonic text DEFAULT ''::text NOT NULL,
CONSTRAINT validation_monotonic_order CHECK ((validation_monotonic = ANY (ARRAY['increasing'::text, 'decreasing'::text, ''::text])))
);
COMMENT ON COLUMN template_version_parameters.name IS 'Parameter name';
@@ -378,6 +380,8 @@ COMMENT ON COLUMN template_version_parameters.validation_max IS 'Validation: max
COMMENT ON COLUMN template_version_parameters.validation_error IS 'Validation: error displayed when the regex does not match.';
COMMENT ON COLUMN template_version_parameters.validation_monotonic IS 'Validation: consecutive values preserve the monotonic order';
CREATE TABLE template_versions (
id uuid NOT NULL,
template_id uuid,
@@ -0,0 +1,3 @@
ALTER TABLE template_version_parameters DROP CONSTRAINT validation_monotonic_order;
ALTER TABLE template_version_parameters DROP COLUMN validation_monotonic;
@@ -0,0 +1,6 @@
ALTER TABLE template_version_parameters ADD COLUMN validation_monotonic text NOT NULL DEFAULT '';
ALTER TABLE template_version_parameters ADD CONSTRAINT validation_monotonic_order CHECK (validation_monotonic IN ('increasing', 'decreasing', ''));
COMMENT ON COLUMN template_version_parameters.validation_monotonic
IS 'Validation: consecutive values preserve the monotonic order';
+2
View File
@@ -1456,6 +1456,8 @@ type TemplateVersionParameter struct {
ValidationMax int32 `db:"validation_max" json:"validation_max"`
// Validation: error displayed when the regex does not match.
ValidationError string `db:"validation_error" json:"validation_error"`
// Validation: consecutive values preserve the monotonic order
ValidationMonotonic string `db:"validation_monotonic" json:"validation_monotonic"`
}
type User struct {
+22 -16
View File
@@ -3519,7 +3519,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
}
const getTemplateVersionParameters = `-- name: GetTemplateVersionParameters :many
SELECT template_version_id, name, description, type, mutable, default_value, icon, options, validation_regex, validation_min, validation_max, validation_error FROM template_version_parameters WHERE template_version_id = $1
SELECT template_version_id, name, description, type, mutable, default_value, icon, options, validation_regex, validation_min, validation_max, validation_error, validation_monotonic FROM template_version_parameters WHERE template_version_id = $1
`
func (q *sqlQuerier) GetTemplateVersionParameters(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionParameter, error) {
@@ -3544,6 +3544,7 @@ func (q *sqlQuerier) GetTemplateVersionParameters(ctx context.Context, templateV
&i.ValidationMin,
&i.ValidationMax,
&i.ValidationError,
&i.ValidationMonotonic,
); err != nil {
return nil, err
}
@@ -3572,7 +3573,8 @@ INSERT INTO
validation_regex,
validation_min,
validation_max,
validation_error
validation_error,
validation_monotonic
)
VALUES
(
@@ -3587,23 +3589,25 @@ VALUES
$9,
$10,
$11,
$12
) RETURNING template_version_id, name, description, type, mutable, default_value, icon, options, validation_regex, validation_min, validation_max, validation_error
$12,
$13
) RETURNING template_version_id, name, description, type, mutable, default_value, icon, options, validation_regex, validation_min, validation_max, validation_error, validation_monotonic
`
type InsertTemplateVersionParameterParams struct {
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
Name string `db:"name" json:"name"`
Description string `db:"description" json:"description"`
Type string `db:"type" json:"type"`
Mutable bool `db:"mutable" json:"mutable"`
DefaultValue string `db:"default_value" json:"default_value"`
Icon string `db:"icon" json:"icon"`
Options json.RawMessage `db:"options" json:"options"`
ValidationRegex string `db:"validation_regex" json:"validation_regex"`
ValidationMin int32 `db:"validation_min" json:"validation_min"`
ValidationMax int32 `db:"validation_max" json:"validation_max"`
ValidationError string `db:"validation_error" json:"validation_error"`
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
Name string `db:"name" json:"name"`
Description string `db:"description" json:"description"`
Type string `db:"type" json:"type"`
Mutable bool `db:"mutable" json:"mutable"`
DefaultValue string `db:"default_value" json:"default_value"`
Icon string `db:"icon" json:"icon"`
Options json.RawMessage `db:"options" json:"options"`
ValidationRegex string `db:"validation_regex" json:"validation_regex"`
ValidationMin int32 `db:"validation_min" json:"validation_min"`
ValidationMax int32 `db:"validation_max" json:"validation_max"`
ValidationError string `db:"validation_error" json:"validation_error"`
ValidationMonotonic string `db:"validation_monotonic" json:"validation_monotonic"`
}
func (q *sqlQuerier) InsertTemplateVersionParameter(ctx context.Context, arg InsertTemplateVersionParameterParams) (TemplateVersionParameter, error) {
@@ -3620,6 +3624,7 @@ func (q *sqlQuerier) InsertTemplateVersionParameter(ctx context.Context, arg Ins
arg.ValidationMin,
arg.ValidationMax,
arg.ValidationError,
arg.ValidationMonotonic,
)
var i TemplateVersionParameter
err := row.Scan(
@@ -3635,6 +3640,7 @@ func (q *sqlQuerier) InsertTemplateVersionParameter(ctx context.Context, arg Ins
&i.ValidationMin,
&i.ValidationMax,
&i.ValidationError,
&i.ValidationMonotonic,
)
return i, err
}
@@ -12,7 +12,8 @@ INSERT INTO
validation_regex,
validation_min,
validation_max,
validation_error
validation_error,
validation_monotonic
)
VALUES
(
@@ -27,7 +28,8 @@ VALUES
$9,
$10,
$11,
$12
$12,
$13
) RETURNING *;
-- name: GetTemplateVersionParameters :many
+13 -12
View File
@@ -649,18 +649,19 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
return nil, xerrors.Errorf("marshal parameter options: %w", err)
}
_, err = server.Database.InsertTemplateVersionParameter(ctx, database.InsertTemplateVersionParameterParams{
TemplateVersionID: input.TemplateVersionID,
Name: richParameter.Name,
Description: richParameter.Description,
Type: richParameter.Type,
Mutable: richParameter.Mutable,
DefaultValue: richParameter.DefaultValue,
Icon: richParameter.Icon,
Options: options,
ValidationRegex: richParameter.ValidationRegex,
ValidationError: richParameter.ValidationError,
ValidationMin: richParameter.ValidationMin,
ValidationMax: richParameter.ValidationMax,
TemplateVersionID: input.TemplateVersionID,
Name: richParameter.Name,
Description: richParameter.Description,
Type: richParameter.Type,
Mutable: richParameter.Mutable,
DefaultValue: richParameter.DefaultValue,
Icon: richParameter.Icon,
Options: options,
ValidationRegex: richParameter.ValidationRegex,
ValidationError: richParameter.ValidationError,
ValidationMin: richParameter.ValidationMin,
ValidationMax: richParameter.ValidationMax,
ValidationMonotonic: richParameter.ValidationMonotonic,
})
if err != nil {
return nil, xerrors.Errorf("insert parameter: %w", err)
+13 -12
View File
@@ -199,7 +199,7 @@ func (api *API) templateVersionSchema(rw http.ResponseWriter, r *http.Request) {
// @Produce json
// @Tags Templates
// @Param templateversion path string true "Template version ID" format(uuid)
// @Success 200 {array} parameter.ComputedValue
// @Success 200 {array} codersdk.TemplateVersionParameter
// @Router /templateversions/{templateversion}/rich-parameters [get]
func (api *API) templateVersionRichParameters(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -1459,17 +1459,18 @@ func convertTemplateVersionParameter(param database.TemplateVersionParameter) (c
})
}
return codersdk.TemplateVersionParameter{
Name: param.Name,
Description: param.Description,
Type: param.Type,
Mutable: param.Mutable,
DefaultValue: param.DefaultValue,
Icon: param.Icon,
Options: options,
ValidationRegex: param.ValidationRegex,
ValidationMin: param.ValidationMin,
ValidationMax: param.ValidationMax,
ValidationError: param.ValidationError,
Name: param.Name,
Description: param.Description,
Type: param.Type,
Mutable: param.Mutable,
DefaultValue: param.DefaultValue,
Icon: param.Icon,
Options: options,
ValidationRegex: param.ValidationRegex,
ValidationMin: param.ValidationMin,
ValidationMax: param.ValidationMax,
ValidationError: param.ValidationError,
ValidationMonotonic: codersdk.ValidationMonotonicOrder(param.ValidationMonotonic),
}, nil
}
+9 -9
View File
@@ -467,15 +467,6 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
return
}
err = codersdk.ValidateWorkspaceBuildParameters(templateVersionParameters, createBuild.RichParameterValues)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Error validating workspace build parameters.",
Detail: err.Error(),
})
return
}
lastBuildParameters, err := api.Database.GetWorkspaceBuildParameters(ctx, priorHistory.ID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
@@ -486,6 +477,15 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
}
apiLastBuildParameters := convertWorkspaceBuildParameters(lastBuildParameters)
err = codersdk.ValidateWorkspaceBuildParameters(templateVersionParameters, createBuild.RichParameterValues, apiLastBuildParameters)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Error validating workspace build parameters.",
Detail: err.Error(),
})
return
}
var parameters []codersdk.WorkspaceBuildParameter
for _, templateVersionParameter := range templateVersionParameters {
// Check if parameter value is in request
+20
View File
@@ -870,6 +870,18 @@ func TestWorkspaceBuildValidateRichParameters(t *testing.T) {
{Name: boolParameterName, Type: "bool", Mutable: true},
}
monotonicIncreasingNumberRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: 3, ValidationMax: 10, ValidationMonotonic: "increasing"},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
monotonicDecreasingNumberRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: 3, ValidationMax: 10, ValidationMonotonic: "decreasing"},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
stringRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true},
@@ -899,6 +911,14 @@ func TestWorkspaceBuildValidateRichParameters(t *testing.T) {
{numberParameterName, "10", true, numberRichParameters},
{numberParameterName, "11", false, numberRichParameters},
{numberParameterName, "6", false, monotonicIncreasingNumberRichParameters},
{numberParameterName, "7", true, monotonicIncreasingNumberRichParameters},
{numberParameterName, "8", true, monotonicIncreasingNumberRichParameters},
{numberParameterName, "6", true, monotonicDecreasingNumberRichParameters},
{numberParameterName, "7", true, monotonicDecreasingNumberRichParameters},
{numberParameterName, "8", false, monotonicDecreasingNumberRichParameters},
{stringParameterName, "", true, stringRichParameters},
{stringParameterName, "foobar", true, stringRichParameters},
+1 -1
View File
@@ -419,7 +419,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
return
}
err = codersdk.ValidateWorkspaceBuildParameters(templateVersionParameters, createWorkspace.RichParameterValues)
err = codersdk.ValidateNewWorkspaceParameters(templateVersionParameters, createWorkspace.RichParameterValues)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Error validating workspace build parameters.",
+24 -5
View File
@@ -1787,12 +1787,15 @@ func TestWorkspaceWithRichParameters(t *testing.T) {
const (
firstParameterName = "first_parameter"
firstParameterType = "string"
firstParameterDescription = "This is first parameter"
firstParameterValue = "1"
secondParameterName = "second_parameter"
secondParameterDescription = "This is second parameter"
secondParameterValue = "2"
secondParameterName = "second_parameter"
secondParameterType = "number"
secondParameterDescription = "This is second parameter"
secondParameterValue = "2"
secondParameterValidationMonotonic = codersdk.MonotonicOrderIncreasing
)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
@@ -1804,8 +1807,19 @@ func TestWorkspaceWithRichParameters(t *testing.T) {
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Parameters: []*proto.RichParameter{
{Name: firstParameterName, Description: firstParameterDescription},
{Name: secondParameterName, Description: secondParameterDescription},
{
Name: firstParameterName,
Type: firstParameterType,
Description: firstParameterDescription,
},
{
Name: secondParameterName,
Type: secondParameterType,
Description: secondParameterDescription,
ValidationMin: 1,
ValidationMax: 3,
ValidationMonotonic: string(secondParameterValidationMonotonic),
},
},
},
},
@@ -1825,7 +1839,12 @@ func TestWorkspaceWithRichParameters(t *testing.T) {
require.NoError(t, err)
require.Len(t, templateRichParameters, 2)
require.Equal(t, templateRichParameters[0].Name, firstParameterName)
require.Equal(t, templateRichParameters[0].Type, firstParameterType)
require.Equal(t, templateRichParameters[0].ValidationMonotonic, codersdk.ValidationMonotonicOrder("")) // no validation for string
require.Equal(t, templateRichParameters[1].Name, secondParameterName)
require.Equal(t, templateRichParameters[1].Type, secondParameterType)
require.Equal(t, templateRichParameters[1].ValidationMonotonic, secondParameterValidationMonotonic)
expectedBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: firstParameterName, Value: firstParameterValue},