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:
@@ -1837,6 +1837,14 @@ func (q *querier) FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context,
|
||||
return q.db.FetchVolumesResourceMonitorsUpdatedAfter(ctx, updatedAt)
|
||||
}
|
||||
|
||||
func (q *querier) FindMatchingPresetID(ctx context.Context, arg database.FindMatchingPresetIDParams) (uuid.UUID, error) {
|
||||
_, err := q.GetTemplateVersionByID(ctx, arg.TemplateVersionID)
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
return q.db.FindMatchingPresetID(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
|
||||
return fetch(q.log, q.auth, q.db.GetAPIKeyByID)(ctx, id)
|
||||
}
|
||||
|
||||
@@ -4965,6 +4965,22 @@ func (s *MethodTestSuite) TestPrebuilds() {
|
||||
template, policy.ActionUse,
|
||||
).Errors(sql.ErrNoRows)
|
||||
}))
|
||||
s.Run("FindMatchingPresetID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
t1 := testutil.Fake(s.T(), faker, database.Template{})
|
||||
tv := testutil.Fake(s.T(), faker, database.TemplateVersion{TemplateID: uuid.NullUUID{UUID: t1.ID, Valid: true}})
|
||||
dbm.EXPECT().FindMatchingPresetID(gomock.Any(), database.FindMatchingPresetIDParams{
|
||||
TemplateVersionID: tv.ID,
|
||||
ParameterNames: []string{"test"},
|
||||
ParameterValues: []string{"test"},
|
||||
}).Return(uuid.Nil, nil).AnyTimes()
|
||||
dbm.EXPECT().GetTemplateVersionByID(gomock.Any(), tv.ID).Return(tv, nil).AnyTimes()
|
||||
dbm.EXPECT().GetTemplateByID(gomock.Any(), t1.ID).Return(t1, nil).AnyTimes()
|
||||
check.Args(database.FindMatchingPresetIDParams{
|
||||
TemplateVersionID: tv.ID,
|
||||
ParameterNames: []string{"test"},
|
||||
ParameterValues: []string{"test"},
|
||||
}).Asserts(tv.RBACObject(t1), policy.ActionRead).Returns(uuid.Nil)
|
||||
}))
|
||||
s.Run("GetPrebuildMetrics", s.Subtest(func(_ database.Store, check *expects) {
|
||||
check.Args().
|
||||
Asserts(rbac.ResourceWorkspace.All(), policy.ActionRead)
|
||||
|
||||
@@ -565,6 +565,13 @@ func (m queryMetricsStore) FetchVolumesResourceMonitorsUpdatedAfter(ctx context.
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) FindMatchingPresetID(ctx context.Context, arg database.FindMatchingPresetIDParams) (uuid.UUID, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.FindMatchingPresetID(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("FindMatchingPresetID").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
|
||||
start := time.Now()
|
||||
apiKey, err := m.s.GetAPIKeyByID(ctx, id)
|
||||
|
||||
@@ -1051,6 +1051,21 @@ func (mr *MockStoreMockRecorder) FetchVolumesResourceMonitorsUpdatedAfter(ctx, u
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchVolumesResourceMonitorsUpdatedAfter", reflect.TypeOf((*MockStore)(nil).FetchVolumesResourceMonitorsUpdatedAfter), ctx, updatedAt)
|
||||
}
|
||||
|
||||
// FindMatchingPresetID mocks base method.
|
||||
func (m *MockStore) FindMatchingPresetID(ctx context.Context, arg database.FindMatchingPresetIDParams) (uuid.UUID, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindMatchingPresetID", ctx, arg)
|
||||
ret0, _ := ret[0].(uuid.UUID)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindMatchingPresetID indicates an expected call of FindMatchingPresetID.
|
||||
func (mr *MockStoreMockRecorder) FindMatchingPresetID(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindMatchingPresetID", reflect.TypeOf((*MockStore)(nil).FindMatchingPresetID), ctx, arg)
|
||||
}
|
||||
|
||||
// GetAPIKeyByID mocks base method.
|
||||
func (m *MockStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -137,6 +137,11 @@ type sqlcQuerier interface {
|
||||
FetchNewMessageMetadata(ctx context.Context, arg FetchNewMessageMetadataParams) (FetchNewMessageMetadataRow, error)
|
||||
FetchVolumesResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceAgentVolumeResourceMonitor, error)
|
||||
FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]WorkspaceAgentVolumeResourceMonitor, error)
|
||||
// FindMatchingPresetID finds a preset ID that is the largest exact subset of the provided parameters.
|
||||
// It returns the preset ID if a match is found, or NULL if no match is found.
|
||||
// The query finds presets where all preset parameters are present in the provided parameters,
|
||||
// and returns the preset with the most parameters (largest subset).
|
||||
FindMatchingPresetID(ctx context.Context, arg FindMatchingPresetIDParams) (uuid.UUID, error)
|
||||
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
|
||||
// there is no unique constraint on empty token names
|
||||
GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error)
|
||||
|
||||
@@ -7252,6 +7252,47 @@ func (q *sqlQuerier) CountInProgressPrebuilds(ctx context.Context) ([]CountInPro
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const findMatchingPresetID = `-- name: FindMatchingPresetID :one
|
||||
WITH provided_params AS (
|
||||
SELECT
|
||||
unnest($1::text[]) AS name,
|
||||
unnest($2::text[]) AS value
|
||||
),
|
||||
preset_matches AS (
|
||||
SELECT
|
||||
tvp.id AS template_version_preset_id,
|
||||
COALESCE(COUNT(tvpp.name), 0) AS total_preset_params,
|
||||
COALESCE(COUNT(pp.name), 0) AS matching_params
|
||||
FROM template_version_presets tvp
|
||||
LEFT JOIN template_version_preset_parameters tvpp ON tvpp.template_version_preset_id = tvp.id
|
||||
LEFT JOIN provided_params pp ON pp.name = tvpp.name AND pp.value = tvpp.value
|
||||
WHERE tvp.template_version_id = $3
|
||||
GROUP BY tvp.id
|
||||
)
|
||||
SELECT pm.template_version_preset_id
|
||||
FROM preset_matches pm
|
||||
WHERE pm.total_preset_params = pm.matching_params -- All preset parameters must match
|
||||
ORDER BY pm.total_preset_params DESC -- Return the preset with the most parameters
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type FindMatchingPresetIDParams struct {
|
||||
ParameterNames []string `db:"parameter_names" json:"parameter_names"`
|
||||
ParameterValues []string `db:"parameter_values" json:"parameter_values"`
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
}
|
||||
|
||||
// FindMatchingPresetID finds a preset ID that is the largest exact subset of the provided parameters.
|
||||
// It returns the preset ID if a match is found, or NULL if no match is found.
|
||||
// The query finds presets where all preset parameters are present in the provided parameters,
|
||||
// and returns the preset with the most parameters (largest subset).
|
||||
func (q *sqlQuerier) FindMatchingPresetID(ctx context.Context, arg FindMatchingPresetIDParams) (uuid.UUID, error) {
|
||||
row := q.db.QueryRowContext(ctx, findMatchingPresetID, pq.Array(arg.ParameterNames), pq.Array(arg.ParameterValues), arg.TemplateVersionID)
|
||||
var template_version_preset_id uuid.UUID
|
||||
err := row.Scan(&template_version_preset_id)
|
||||
return template_version_preset_id, err
|
||||
}
|
||||
|
||||
const getPrebuildMetrics = `-- name: GetPrebuildMetrics :many
|
||||
SELECT
|
||||
t.name as template_name,
|
||||
|
||||
@@ -245,3 +245,30 @@ INNER JOIN organizations o ON o.id = w.organization_id
|
||||
WHERE NOT t.deleted AND wpb.build_number = 1
|
||||
GROUP BY t.name, tvp.name, o.name
|
||||
ORDER BY t.name, tvp.name, o.name;
|
||||
|
||||
-- name: FindMatchingPresetID :one
|
||||
-- FindMatchingPresetID finds a preset ID that is the largest exact subset of the provided parameters.
|
||||
-- It returns the preset ID if a match is found, or NULL if no match is found.
|
||||
-- The query finds presets where all preset parameters are present in the provided parameters,
|
||||
-- and returns the preset with the most parameters (largest subset).
|
||||
WITH provided_params AS (
|
||||
SELECT
|
||||
unnest(@parameter_names::text[]) AS name,
|
||||
unnest(@parameter_values::text[]) AS value
|
||||
),
|
||||
preset_matches AS (
|
||||
SELECT
|
||||
tvp.id AS template_version_preset_id,
|
||||
COALESCE(COUNT(tvpp.name), 0) AS total_preset_params,
|
||||
COALESCE(COUNT(pp.name), 0) AS matching_params
|
||||
FROM template_version_presets tvp
|
||||
LEFT JOIN template_version_preset_parameters tvpp ON tvpp.template_version_preset_id = tvp.id
|
||||
LEFT JOIN provided_params pp ON pp.name = tvpp.name AND pp.value = tvpp.value
|
||||
WHERE tvp.template_version_id = @template_version_id
|
||||
GROUP BY tvp.id
|
||||
)
|
||||
SELECT pm.template_version_preset_id
|
||||
FROM preset_matches pm
|
||||
WHERE pm.total_preset_params = pm.matching_params -- All preset parameters must match
|
||||
ORDER BY pm.total_preset_params DESC -- Return the preset with the most parameters
|
||||
LIMIT 1;
|
||||
|
||||
Reference in New Issue
Block a user