refactor: workspace builds (#7541)

* refactor workspace builds

Signed-off-by: Spike Curtis <spike@coder.com>

* make gen

Signed-off-by: Spike Curtis <spike@coder.com>

* Remove ParameterResolver from typescript

Signed-off-by: Spike Curtis <spike@coder.com>

* rename conversion -> database/db2sdk

Signed-off-by: Spike Curtis <spike@coder.com>

* tests for db2sdk

Signed-off-by: Spike Curtis <spike@coder.com>

* Tests for ParameterResolver

Signed-off-by: Spike Curtis <spike@coder.com>

* wsbuilder tests

Signed-off-by: Spike Curtis <spike@coder.com>

* Move parameter validation tests to richparameters_test.go

Signed-off-by: Spike Curtis <spike@coder.com>

* Fix CI generation; rename mock->dbmock

Signed-off-by: Spike Curtis <spike@coder.com>

* Fix test imports

Signed-off-by: Spike Curtis <spike@coder.com>

---------

Signed-off-by: Spike Curtis <spike@coder.com>
This commit is contained in:
Spike Curtis
2023-05-23 12:06:33 +04:00
committed by GitHub
parent 622456faf8
commit cd416c86dd
31 changed files with 5681 additions and 1238 deletions
+1 -1
View File
@@ -43,7 +43,6 @@ const (
type ComputedParameter struct {
Parameter
SourceValue string `json:"source_value"`
SchemaID uuid.UUID `json:"schema_id" format:"uuid"`
DefaultSourceValue bool `json:"default_source_value"`
}
@@ -60,6 +59,7 @@ type Parameter struct {
DestinationScheme ParameterDestinationScheme `json:"destination_scheme" table:"destination scheme" validate:"ne=none" enums:"none,environment_variable,provisioner_variable"`
CreatedAt time.Time `json:"created_at" table:"created at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" table:"updated at" format:"date-time"`
SourceValue string `json:"source_value"`
}
type ParameterSchema struct {
+61
View File
@@ -116,3 +116,64 @@ func validationEnabled(param TemplateVersionParameter) bool {
param.Type == "bool" || // boolean type doesn't have any custom validation rules, but the value must be checked (true/false).
param.Type == "list(string)" // list(string) type doesn't have special validation, but we need to check if this is a correct list.
}
// ParameterResolver should be populated with legacy workload and rich parameter values from the previous build. It then
// supports queries against a current TemplateVersionParameter to determine whether a new value is required, or a value
// correctly validates.
// @typescript-ignore ParameterResolver
type ParameterResolver struct {
Legacy []Parameter
Rich []WorkspaceBuildParameter
}
// ValidateResolve checks the provided value, v, against the parameter, p, and the previous build. If v is nil, it also
// resolves the correct value. It returns the value of the parameter, if valid, and an error if invalid.
func (r *ParameterResolver) ValidateResolve(p TemplateVersionParameter, v *WorkspaceBuildParameter) (value string, err error) {
prevV := r.findLastValue(p)
if !p.Mutable && v != nil && prevV != nil {
return "", xerrors.Errorf("Parameter %q is not mutable, so it can't be updated after creating a workspace.", p.Name)
}
if p.Required && v == nil && prevV == nil {
return "", xerrors.Errorf("Parameter %q is required but not provided", p.Name)
}
// First, the provided value
resolvedValue := v
// Second, previous value
if resolvedValue == nil {
resolvedValue = prevV
}
// Last, default value
if resolvedValue == nil {
resolvedValue = &WorkspaceBuildParameter{
Name: p.Name,
Value: p.DefaultValue,
}
}
err = ValidateWorkspaceBuildParameter(p, resolvedValue, prevV)
if err != nil {
return "", err
}
return resolvedValue.Value, nil
}
// findLastValue finds the value from the previous build and returns it, or nil if the parameter had no value in the
// last build.
func (r *ParameterResolver) findLastValue(p TemplateVersionParameter) *WorkspaceBuildParameter {
for _, rp := range r.Rich {
if rp.Name == p.Name {
return &rp
}
}
// For migration purposes, we also support using a legacy variable
if p.LegacyVariableName != "" {
for _, lp := range r.Legacy {
if lp.Name == p.LegacyVariableName {
return &WorkspaceBuildParameter{
Name: p.Name,
Value: lp.SourceValue,
}
}
}
}
return nil
}
+324
View File
@@ -0,0 +1,324 @@
package codersdk_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/codersdk"
)
func TestParameterResolver_ValidateResolve_New(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
}
v, err := uut.ValidateResolve(p, &codersdk.WorkspaceBuildParameter{
Name: "n",
Value: "1",
})
require.NoError(t, err)
require.Equal(t, "1", v)
}
func TestParameterResolver_ValidateResolve_Default(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
DefaultValue: "5",
}
v, err := uut.ValidateResolve(p, nil)
require.NoError(t, err)
require.Equal(t, "5", v)
}
func TestParameterResolver_ValidateResolve_MissingRequired(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
Required: true,
}
v, err := uut.ValidateResolve(p, nil)
require.Error(t, err)
require.Equal(t, "", v)
}
func TestParameterResolver_ValidateResolve_PrevRequired(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{
Rich: []codersdk.WorkspaceBuildParameter{{Name: "n", Value: "5"}},
}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
Required: true,
}
v, err := uut.ValidateResolve(p, nil)
require.NoError(t, err)
require.Equal(t, "5", v)
}
func TestParameterResolver_ValidateResolve_PrevInvalid(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{
Rich: []codersdk.WorkspaceBuildParameter{{Name: "n", Value: "11"}},
}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
ValidationMax: 10,
ValidationMin: 1,
}
v, err := uut.ValidateResolve(p, nil)
require.Error(t, err)
require.Equal(t, "", v)
}
func TestParameterResolver_ValidateResolve_DefaultInvalid(t *testing.T) {
// this one arises from an error on the template itself, where the default
// value doesn't pass validation. But, it's good to catch early and error out
// rather than send invalid data to the provisioner
t.Parallel()
uut := codersdk.ParameterResolver{}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
ValidationMax: 10,
ValidationMin: 1,
DefaultValue: "11",
}
v, err := uut.ValidateResolve(p, nil)
require.Error(t, err)
require.Equal(t, "", v)
}
func TestParameterResolver_ValidateResolve_NewOverridesOld(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{
Rich: []codersdk.WorkspaceBuildParameter{{Name: "n", Value: "5"}},
}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
Required: true,
Mutable: true,
}
v, err := uut.ValidateResolve(p, &codersdk.WorkspaceBuildParameter{
Name: "n",
Value: "6",
})
require.NoError(t, err)
require.Equal(t, "6", v)
}
func TestParameterResolver_ValidateResolve_Immutable(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{
Rich: []codersdk.WorkspaceBuildParameter{{Name: "n", Value: "5"}},
}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
Required: true,
Mutable: false,
}
v, err := uut.ValidateResolve(p, &codersdk.WorkspaceBuildParameter{
Name: "n",
Value: "6",
})
require.Error(t, err)
require.Equal(t, "", v)
}
func TestParameterResolver_ValidateResolve_Legacy(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{
Legacy: []codersdk.Parameter{
{Name: "l", SourceValue: "5"},
{Name: "n", SourceValue: "6"},
},
}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
Required: true,
LegacyVariableName: "l",
}
v, err := uut.ValidateResolve(p, nil)
require.NoError(t, err)
require.Equal(t, "5", v)
}
func TestParameterResolver_ValidateResolve_PreferRichOverLegacy(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{
Rich: []codersdk.WorkspaceBuildParameter{{Name: "n", Value: "7"}},
Legacy: []codersdk.Parameter{
{Name: "l", SourceValue: "5"},
{Name: "n", SourceValue: "6"},
},
}
p := codersdk.TemplateVersionParameter{
Name: "n",
Type: "number",
Required: true,
LegacyVariableName: "l",
}
v, err := uut.ValidateResolve(p, nil)
require.NoError(t, err)
require.Equal(t, "7", v)
}
func TestRichParameterValidation(t *testing.T) {
t.Parallel()
const (
stringParameterName = "string_parameter"
stringParameterValue = "abc"
numberParameterName = "number_parameter"
numberParameterValue = "7"
boolParameterName = "bool_parameter"
boolParameterValue = "true"
listOfStringsParameterName = "list_of_strings_parameter"
listOfStringsParameterValue = `["a","b","c"]`
)
initialBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: stringParameterName, Value: stringParameterValue},
{Name: numberParameterName, Value: numberParameterValue},
{Name: boolParameterName, Value: boolParameterValue},
{Name: listOfStringsParameterName, Value: listOfStringsParameterValue},
}
t.Run("NoValidation", func(t *testing.T) {
t.Parallel()
p := codersdk.TemplateVersionParameter{
Name: numberParameterName, Type: "number", Mutable: true,
}
uut := codersdk.ParameterResolver{
Rich: initialBuildParameters,
}
v, err := uut.ValidateResolve(p, &codersdk.WorkspaceBuildParameter{
Name: numberParameterName, Value: "42",
})
require.NoError(t, err)
require.Equal(t, v, "42")
})
t.Run("Validation", func(t *testing.T) {
t.Parallel()
numberRichParameters := []codersdk.TemplateVersionParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: 3, ValidationMax: 10},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
monotonicIncreasingNumberRichParameters := []codersdk.TemplateVersionParameter{
{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 := []codersdk.TemplateVersionParameter{
{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 := []codersdk.TemplateVersionParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
boolRichParameters := []codersdk.TemplateVersionParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
regexRichParameters := []codersdk.TemplateVersionParameter{
{Name: stringParameterName, Type: "string", Mutable: true, ValidationRegex: "^[a-z]+$", ValidationError: "this is error"},
{Name: numberParameterName, Type: "number", Mutable: true},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
listOfStringsRichParameters := []codersdk.TemplateVersionParameter{
{Name: listOfStringsParameterName, Type: "list(string)", Mutable: true},
}
tests := []struct {
parameterName string
value string
valid bool
richParameters []codersdk.TemplateVersionParameter
}{
{numberParameterName, "2", false, numberRichParameters},
{numberParameterName, "3", true, numberRichParameters},
{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},
{stringParameterName, "abcd", true, regexRichParameters},
{stringParameterName, "abcd1", false, regexRichParameters},
{boolParameterName, "true", true, boolRichParameters},
{boolParameterName, "false", true, boolRichParameters},
{boolParameterName, "cat", false, boolRichParameters},
{listOfStringsParameterName, `[]`, true, listOfStringsRichParameters},
{listOfStringsParameterName, `["aa"]`, true, listOfStringsRichParameters},
{listOfStringsParameterName, `["aa]`, false, listOfStringsRichParameters},
{listOfStringsParameterName, ``, false, listOfStringsRichParameters},
}
for _, tc := range tests {
tc := tc
t.Run(tc.parameterName+"-"+tc.value, func(t *testing.T) {
t.Parallel()
uut := codersdk.ParameterResolver{
Rich: initialBuildParameters,
}
for _, p := range tc.richParameters {
if p.Name != tc.parameterName {
continue
}
v, err := uut.ValidateResolve(p, &codersdk.WorkspaceBuildParameter{
Name: tc.parameterName,
Value: tc.value,
})
if tc.valid {
require.NoError(t, err)
require.Equal(t, tc.value, v)
} else {
require.Error(t, err)
}
}
})
}
})
}