mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: claim prebuilds based on workspace parameters instead of preset id (#19279)
Closes https://github.com/coder/coder/issues/18356. This change finds and selects a matching preset if one was not chosen during workspace creation. This solidifies the relationship between presets and parameters. When a workspace is created without in explicitly chosen preset, it will now still be eligible to claim a prebuilt workspace if one is available.
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package prebuilds
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
)
|
||||
|
||||
// FindMatchingPresetID finds a preset ID that matches the provided parameters.
|
||||
// It returns the preset ID if a match is found, or uuid.Nil if no match is found.
|
||||
// The function performs a bidirectional comparison to ensure all parameters match exactly.
|
||||
func FindMatchingPresetID(
|
||||
ctx context.Context,
|
||||
store database.Store,
|
||||
templateVersionID uuid.UUID,
|
||||
parameterNames []string,
|
||||
parameterValues []string,
|
||||
) (uuid.UUID, error) {
|
||||
if len(parameterNames) != len(parameterValues) {
|
||||
return uuid.Nil, xerrors.New("parameter names and values must have the same length")
|
||||
}
|
||||
|
||||
result, err := store.FindMatchingPresetID(ctx, database.FindMatchingPresetIDParams{
|
||||
TemplateVersionID: templateVersionID,
|
||||
ParameterNames: parameterNames,
|
||||
ParameterValues: parameterValues,
|
||||
})
|
||||
if err != nil {
|
||||
// Handle the case where no matching preset is found (no rows returned)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return uuid.Nil, nil
|
||||
}
|
||||
return uuid.Nil, xerrors.Errorf("find matching preset ID: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package prebuilds_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/prebuilds"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestFindMatchingPresetID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
presetIDs := []uuid.UUID{
|
||||
uuid.New(),
|
||||
uuid.New(),
|
||||
}
|
||||
// Give each preset a meaningful name in alphabetical order
|
||||
presetNames := map[uuid.UUID]string{
|
||||
presetIDs[0]: "development",
|
||||
presetIDs[1]: "production",
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
parameterNames []string
|
||||
parameterValues []string
|
||||
presetParameters []database.TemplateVersionPresetParameter
|
||||
expectedPresetID uuid.UUID
|
||||
expectError bool
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "exact match",
|
||||
parameterNames: []string{"region", "instance_type"},
|
||||
parameterValues: []string{"us-west-2", "t3.medium"},
|
||||
presetParameters: []database.TemplateVersionPresetParameter{
|
||||
{TemplateVersionPresetID: presetIDs[0], Name: "region", Value: "us-west-2"},
|
||||
{TemplateVersionPresetID: presetIDs[0], Name: "instance_type", Value: "t3.medium"},
|
||||
// antagonist:
|
||||
{TemplateVersionPresetID: presetIDs[1], Name: "region", Value: "us-west-2"},
|
||||
{TemplateVersionPresetID: presetIDs[1], Name: "instance_type", Value: "t3.large"},
|
||||
},
|
||||
expectedPresetID: presetIDs[0],
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "no match - different values",
|
||||
parameterNames: []string{"region", "instance_type"},
|
||||
parameterValues: []string{"us-east-1", "t3.medium"},
|
||||
presetParameters: []database.TemplateVersionPresetParameter{
|
||||
{TemplateVersionPresetID: presetIDs[0], Name: "region", Value: "us-west-2"},
|
||||
{TemplateVersionPresetID: presetIDs[0], Name: "instance_type", Value: "t3.medium"},
|
||||
// antagonist:
|
||||
{TemplateVersionPresetID: presetIDs[1], Name: "region", Value: "us-west-2"},
|
||||
{TemplateVersionPresetID: presetIDs[1], Name: "instance_type", Value: "t3.large"},
|
||||
},
|
||||
expectedPresetID: uuid.Nil,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "no match - fewer provided parameters",
|
||||
parameterNames: []string{"region"},
|
||||
parameterValues: []string{"us-west-2"},
|
||||
presetParameters: []database.TemplateVersionPresetParameter{
|
||||
{TemplateVersionPresetID: presetIDs[0], Name: "region", Value: "us-west-2"},
|
||||
{TemplateVersionPresetID: presetIDs[0], Name: "instance_type", Value: "t3.medium"},
|
||||
// antagonist:
|
||||
{TemplateVersionPresetID: presetIDs[1], Name: "region", Value: "us-west-2"},
|
||||
{TemplateVersionPresetID: presetIDs[1], Name: "instance_type", Value: "t3.large"},
|
||||
},
|
||||
expectedPresetID: uuid.Nil,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "subset match - extra provided parameter",
|
||||
parameterNames: []string{"region", "instance_type", "extra_param"},
|
||||
parameterValues: []string{"us-west-2", "t3.medium", "extra_value"},
|
||||
presetParameters: []database.TemplateVersionPresetParameter{
|
||||
{TemplateVersionPresetID: presetIDs[0], Name: "region", Value: "us-west-2"},
|
||||
{TemplateVersionPresetID: presetIDs[0], Name: "instance_type", Value: "t3.medium"},
|
||||
// antagonist:
|
||||
{TemplateVersionPresetID: presetIDs[1], Name: "region", Value: "us-west-2"},
|
||||
{TemplateVersionPresetID: presetIDs[1], Name: "instance_type", Value: "t3.large"},
|
||||
},
|
||||
expectedPresetID: presetIDs[0], // Should match because all preset parameters are present
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "mismatched parameter names vs values",
|
||||
parameterNames: []string{"region", "instance_type"},
|
||||
parameterValues: []string{"us-west-2"},
|
||||
presetParameters: []database.TemplateVersionPresetParameter{},
|
||||
expectedPresetID: uuid.Nil,
|
||||
expectError: true,
|
||||
errorContains: "parameter names and values must have the same length",
|
||||
},
|
||||
{
|
||||
name: "multiple presets - match first",
|
||||
parameterNames: []string{"region", "instance_type"},
|
||||
parameterValues: []string{"us-west-2", "t3.medium"},
|
||||
presetParameters: []database.TemplateVersionPresetParameter{
|
||||
{TemplateVersionPresetID: presetIDs[0], Name: "region", Value: "us-west-2"},
|
||||
{TemplateVersionPresetID: presetIDs[0], Name: "instance_type", Value: "t3.medium"},
|
||||
{TemplateVersionPresetID: presetIDs[1], Name: "region", Value: "us-east-1"},
|
||||
{TemplateVersionPresetID: presetIDs[1], Name: "instance_type", Value: "t3.large"},
|
||||
},
|
||||
expectedPresetID: presetIDs[0],
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "largest subset match",
|
||||
parameterNames: []string{"region", "instance_type", "storage_size"},
|
||||
parameterValues: []string{"us-west-2", "t3.medium", "100gb"},
|
||||
presetParameters: []database.TemplateVersionPresetParameter{
|
||||
{TemplateVersionPresetID: presetIDs[0], Name: "region", Value: "us-west-2"},
|
||||
{TemplateVersionPresetID: presetIDs[0], Name: "instance_type", Value: "t3.medium"},
|
||||
{TemplateVersionPresetID: presetIDs[1], Name: "region", Value: "us-west-2"},
|
||||
},
|
||||
expectedPresetID: presetIDs[0], // Should match the larger subset (2 params vs 1 param)
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
JobID: uuid.New(),
|
||||
})
|
||||
|
||||
// Group parameters by preset ID and create presets
|
||||
presetMap := make(map[uuid.UUID][]database.TemplateVersionPresetParameter)
|
||||
for _, param := range tt.presetParameters {
|
||||
presetMap[param.TemplateVersionPresetID] = append(presetMap[param.TemplateVersionPresetID], param)
|
||||
}
|
||||
|
||||
// Create presets and insert their parameters
|
||||
for presetID, params := range presetMap {
|
||||
// Create the preset
|
||||
_, err := db.InsertPreset(ctx, database.InsertPresetParams{
|
||||
ID: presetID,
|
||||
TemplateVersionID: templateVersion.ID,
|
||||
Name: presetNames[presetID],
|
||||
CreatedAt: dbtestutil.NowInDefaultTimezone(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert parameters for this preset
|
||||
names := make([]string, len(params))
|
||||
values := make([]string, len(params))
|
||||
for i, param := range params {
|
||||
names[i] = param.Name
|
||||
values[i] = param.Value
|
||||
}
|
||||
|
||||
_, err = db.InsertPresetParameters(ctx, database.InsertPresetParametersParams{
|
||||
TemplateVersionPresetID: presetID,
|
||||
Names: names,
|
||||
Values: values,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
result, err := prebuilds.FindMatchingPresetID(
|
||||
ctx,
|
||||
db,
|
||||
templateVersion.ID,
|
||||
tt.parameterNames,
|
||||
tt.parameterValues,
|
||||
)
|
||||
|
||||
// Assert results
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
if tt.errorContains != "" {
|
||||
assert.Contains(t, err.Error(), tt.errorContains)
|
||||
}
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedPresetID, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user