Skip to content

Commit e999a58

Browse files
committed
Implement preset auto match
1 parent d3d8948 commit e999a58

File tree

13 files changed

+680
-48
lines changed

13 files changed

+680
-48
lines changed

coderd/database/dbauthz/dbauthz.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1810,6 +1810,14 @@ func (q *querier) FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context,
18101810
return q.db.FetchVolumesResourceMonitorsUpdatedAfter(ctx, updatedAt)
18111811
}
18121812

1813+
func (q *querier) FindMatchingPresetID(ctx context.Context, arg database.FindMatchingPresetIDParams) (uuid.UUID, error) {
1814+
_, err := q.GetTemplateVersionByID(ctx, arg.TemplateVersionID)
1815+
if err != nil {
1816+
return uuid.Nil, err
1817+
}
1818+
return q.db.FindMatchingPresetID(ctx, arg)
1819+
}
1820+
18131821
func (q *querier) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
18141822
return fetch(q.log, q.auth, q.db.GetAPIKeyByID)(ctx, id)
18151823
}

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4959,6 +4959,13 @@ func (s *MethodTestSuite) TestPrebuilds() {
49594959
template, policy.ActionUse,
49604960
).Errors(sql.ErrNoRows)
49614961
}))
4962+
s.Run("FindMatchingPresetID", s.Subtest(func(db database.Store, check *expects) {
4963+
check.Args(database.FindMatchingPresetIDParams{
4964+
TemplateVersionID: uuid.New(),
4965+
ParameterNames: []string{"test"},
4966+
ParameterValues: []string{"test"},
4967+
}).Asserts(rbac.ResourceTemplate, policy.ActionRead)
4968+
}))
49624969
s.Run("GetPrebuildMetrics", s.Subtest(func(_ database.Store, check *expects) {
49634970
check.Args().
49644971
Asserts(rbac.ResourceWorkspace.All(), policy.ActionRead)

coderd/database/dbmetrics/querymetrics.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/querier.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/prebuilds.sql

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,29 @@ INNER JOIN organizations o ON o.id = w.organization_id
245245
WHERE NOT t.deleted AND wpb.build_number = 1
246246
GROUP BY t.name, tvp.name, o.name
247247
ORDER BY t.name, tvp.name, o.name;
248+
249+
-- name: FindMatchingPresetID :one
250+
-- FindMatchingPresetID finds a preset ID that is the largest exact subset of the provided parameters.
251+
-- It returns the preset ID if a match is found, or NULL if no match is found.
252+
-- The query finds presets where all preset parameters are present in the provided parameters,
253+
-- and returns the preset with the most parameters (largest subset).
254+
WITH provided_params AS (
255+
SELECT unnest(@parameter_names::text[]) AS name,
256+
unnest(@parameter_values::text[]) AS value
257+
),
258+
preset_matches AS (
259+
SELECT
260+
tvp.id AS template_version_preset_id,
261+
COALESCE(COUNT(tvpp.name), 0) AS total_preset_params,
262+
COALESCE(COUNT(pp.name), 0) AS matching_params
263+
FROM template_version_presets tvp
264+
LEFT JOIN template_version_preset_parameters tvpp ON tvpp.template_version_preset_id = tvp.id
265+
LEFT JOIN provided_params pp ON pp.name = tvpp.name AND pp.value = tvpp.value
266+
WHERE tvp.template_version_id = @template_version_id
267+
GROUP BY tvp.id
268+
)
269+
SELECT pm.template_version_preset_id
270+
FROM preset_matches pm
271+
WHERE pm.total_preset_params = pm.matching_params -- All preset parameters must match
272+
ORDER BY pm.total_preset_params DESC -- Return the preset with the most parameters
273+
LIMIT 1;

coderd/prebuilds/parameters.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package prebuilds
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
8+
"github.com/google/uuid"
9+
"golang.org/x/xerrors"
10+
11+
"github.com/coder/coder/v2/coderd/database"
12+
)
13+
14+
// FindMatchingPresetID finds a preset ID that matches the provided parameters.
15+
// It returns the preset ID if a match is found, or uuid.Nil if no match is found.
16+
// The function performs a bidirectional comparison to ensure all parameters match exactly.
17+
func FindMatchingPresetID(
18+
ctx context.Context,
19+
store database.Store,
20+
templateVersionID uuid.UUID,
21+
parameterNames []string,
22+
parameterValues []string,
23+
) (uuid.UUID, error) {
24+
if len(parameterNames) != len(parameterValues) {
25+
return uuid.Nil, xerrors.New("parameter names and values must have the same length")
26+
}
27+
28+
result, err := store.FindMatchingPresetID(ctx, database.FindMatchingPresetIDParams{
29+
TemplateVersionID: templateVersionID,
30+
ParameterNames: parameterNames,
31+
ParameterValues: parameterValues,
32+
})
33+
if err != nil {
34+
// Handle the case where no matching preset is found (no rows returned)
35+
if errors.Is(err, sql.ErrNoRows) {
36+
return uuid.Nil, nil
37+
}
38+
return uuid.Nil, xerrors.Errorf("find matching preset ID: %w", err)
39+
}
40+
41+
return result, nil
42+
}

coderd/prebuilds/parameters_test.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package prebuilds_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/uuid"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/coder/v2/coderd/database"
11+
"github.com/coder/coder/v2/coderd/database/dbgen"
12+
"github.com/coder/coder/v2/coderd/database/dbtestutil"
13+
"github.com/coder/coder/v2/coderd/prebuilds"
14+
"github.com/coder/coder/v2/testutil"
15+
)
16+
17+
func TestFindMatchingPresetID(t *testing.T) {
18+
t.Parallel()
19+
20+
presetIDs := []uuid.UUID{
21+
uuid.New(),
22+
uuid.New(),
23+
}
24+
// Give each preset a meaningful name in alphabetical order
25+
presetNames := map[uuid.UUID]string{
26+
presetIDs[0]: "development",
27+
presetIDs[1]: "production",
28+
}
29+
tests := []struct {
30+
name string
31+
parameterNames []string
32+
parameterValues []string
33+
presetParameters []database.TemplateVersionPresetParameter
34+
expectedPresetID uuid.UUID
35+
expectError bool
36+
errorContains string
37+
}{
38+
{
39+
name: "exact match",
40+
parameterNames: []string{"region", "instance_type"},
41+
parameterValues: []string{"us-west-2", "t3.medium"},
42+
presetParameters: []database.TemplateVersionPresetParameter{
43+
{TemplateVersionPresetID: presetIDs[0], Name: "region", Value: "us-west-2"},
44+
{TemplateVersionPresetID: presetIDs[0], Name: "instance_type", Value: "t3.medium"},
45+
// antagonist:
46+
{TemplateVersionPresetID: presetIDs[1], Name: "region", Value: "us-west-2"},
47+
{TemplateVersionPresetID: presetIDs[1], Name: "instance_type", Value: "t3.large"},
48+
},
49+
expectedPresetID: presetIDs[0],
50+
expectError: false,
51+
},
52+
{
53+
name: "no match - different values",
54+
parameterNames: []string{"region", "instance_type"},
55+
parameterValues: []string{"us-east-1", "t3.medium"},
56+
presetParameters: []database.TemplateVersionPresetParameter{
57+
{TemplateVersionPresetID: presetIDs[0], Name: "region", Value: "us-west-2"},
58+
{TemplateVersionPresetID: presetIDs[0], Name: "instance_type", Value: "t3.medium"},
59+
// antagonist:
60+
{TemplateVersionPresetID: presetIDs[1], Name: "region", Value: "us-west-2"},
61+
{TemplateVersionPresetID: presetIDs[1], Name: "instance_type", Value: "t3.large"},
62+
},
63+
expectedPresetID: uuid.Nil,
64+
expectError: false,
65+
},
66+
{
67+
name: "no match - fewer provided parameters",
68+
parameterNames: []string{"region"},
69+
parameterValues: []string{"us-west-2"},
70+
presetParameters: []database.TemplateVersionPresetParameter{
71+
{TemplateVersionPresetID: presetIDs[0], Name: "region", Value: "us-west-2"},
72+
{TemplateVersionPresetID: presetIDs[0], Name: "instance_type", Value: "t3.medium"},
73+
// antagonist:
74+
{TemplateVersionPresetID: presetIDs[1], Name: "region", Value: "us-west-2"},
75+
{TemplateVersionPresetID: presetIDs[1], Name: "instance_type", Value: "t3.large"},
76+
},
77+
expectedPresetID: uuid.Nil,
78+
expectError: false,
79+
},
80+
{
81+
name: "subset match - extra provided parameter",
82+
parameterNames: []string{"region", "instance_type", "extra_param"},
83+
parameterValues: []string{"us-west-2", "t3.medium", "extra_value"},
84+
presetParameters: []database.TemplateVersionPresetParameter{
85+
{TemplateVersionPresetID: presetIDs[0], Name: "region", Value: "us-west-2"},
86+
{TemplateVersionPresetID: presetIDs[0], Name: "instance_type", Value: "t3.medium"},
87+
// antagonist:
88+
{TemplateVersionPresetID: presetIDs[1], Name: "region", Value: "us-west-2"},
89+
{TemplateVersionPresetID: presetIDs[1], Name: "instance_type", Value: "t3.large"},
90+
},
91+
expectedPresetID: presetIDs[0], // Should match because all preset parameters are present
92+
expectError: false,
93+
},
94+
{
95+
name: "mismatched parameter names vs values",
96+
parameterNames: []string{"region", "instance_type"},
97+
parameterValues: []string{"us-west-2"},
98+
presetParameters: []database.TemplateVersionPresetParameter{},
99+
expectedPresetID: uuid.Nil,
100+
expectError: true,
101+
errorContains: "parameter names and values must have the same length",
102+
},
103+
{
104+
name: "multiple presets - match first",
105+
parameterNames: []string{"region", "instance_type"},
106+
parameterValues: []string{"us-west-2", "t3.medium"},
107+
presetParameters: []database.TemplateVersionPresetParameter{
108+
{TemplateVersionPresetID: presetIDs[0], Name: "region", Value: "us-west-2"},
109+
{TemplateVersionPresetID: presetIDs[0], Name: "instance_type", Value: "t3.medium"},
110+
{TemplateVersionPresetID: presetIDs[1], Name: "region", Value: "us-east-1"},
111+
{TemplateVersionPresetID: presetIDs[1], Name: "instance_type", Value: "t3.large"},
112+
},
113+
expectedPresetID: presetIDs[0],
114+
expectError: false,
115+
},
116+
{
117+
name: "largest subset match",
118+
parameterNames: []string{"region", "instance_type", "storage_size"},
119+
parameterValues: []string{"us-west-2", "t3.medium", "100gb"},
120+
presetParameters: []database.TemplateVersionPresetParameter{
121+
{TemplateVersionPresetID: presetIDs[0], Name: "region", Value: "us-west-2"},
122+
{TemplateVersionPresetID: presetIDs[0], Name: "instance_type", Value: "t3.medium"},
123+
{TemplateVersionPresetID: presetIDs[1], Name: "region", Value: "us-west-2"},
124+
},
125+
expectedPresetID: presetIDs[0], // Should match the larger subset (2 params vs 1 param)
126+
expectError: false,
127+
},
128+
}
129+
130+
for _, tt := range tests {
131+
tt := tt
132+
t.Run(tt.name, func(t *testing.T) {
133+
t.Parallel()
134+
135+
ctx := testutil.Context(t, testutil.WaitShort)
136+
db, _ := dbtestutil.NewDB(t)
137+
org := dbgen.Organization(t, db, database.Organization{})
138+
user := dbgen.User(t, db, database.User{})
139+
templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{
140+
OrganizationID: org.ID,
141+
CreatedBy: user.ID,
142+
JobID: uuid.New(),
143+
})
144+
145+
// Group parameters by preset ID and create presets
146+
presetMap := make(map[uuid.UUID][]database.TemplateVersionPresetParameter)
147+
for _, param := range tt.presetParameters {
148+
presetMap[param.TemplateVersionPresetID] = append(presetMap[param.TemplateVersionPresetID], param)
149+
}
150+
151+
// Create presets and insert their parameters
152+
for presetID, params := range presetMap {
153+
// Create the preset
154+
_, err := db.InsertPreset(ctx, database.InsertPresetParams{
155+
ID: presetID,
156+
TemplateVersionID: templateVersion.ID,
157+
Name: presetNames[presetID],
158+
CreatedAt: dbtestutil.NowInDefaultTimezone(),
159+
})
160+
require.NoError(t, err)
161+
162+
// Insert parameters for this preset
163+
names := make([]string, len(params))
164+
values := make([]string, len(params))
165+
for i, param := range params {
166+
names[i] = param.Name
167+
values[i] = param.Value
168+
}
169+
170+
_, err = db.InsertPresetParameters(ctx, database.InsertPresetParametersParams{
171+
TemplateVersionPresetID: presetID,
172+
Names: names,
173+
Values: values,
174+
})
175+
require.NoError(t, err)
176+
}
177+
178+
result, err := prebuilds.FindMatchingPresetID(
179+
ctx,
180+
db,
181+
templateVersion.ID,
182+
tt.parameterNames,
183+
tt.parameterValues,
184+
)
185+
186+
// Assert results
187+
if tt.expectError {
188+
require.Error(t, err)
189+
if tt.errorContains != "" {
190+
assert.Contains(t, err.Error(), tt.errorContains)
191+
}
192+
} else {
193+
require.NoError(t, err)
194+
assert.Equal(t, tt.expectedPresetID, result)
195+
}
196+
})
197+
}
198+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy