mirror of
https://github.com/coder/coder.git
synced 2026-10-29 17:44:33 +08:00
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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user