Skip to content

feat(coderd): update endpoint to allow filtering provisioner daemons by tags #15448

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
3d45f55
feat(coderd/database): allow filtering provisioner daemons by tags
SasSwart Nov 8, 2024
92b9d25
feat(coderd/database): allow filtering provisioner daemons by tags
SasSwart Nov 8, 2024
e68a8fc
add a type hint for sqlc
SasSwart Nov 8, 2024
93058e6
ensure BC with GetProvisionerDaemonsByOrganization
SasSwart Nov 8, 2024
a2bfae3
add a type hint for sqlc
SasSwart Nov 11, 2024
39ab8a0
fix unit test
SasSwart Nov 11, 2024
8bf79c8
Add tags param to provisioner daemons endpoint
SasSwart Nov 11, 2024
86f7dab
fix provisioner tag filtering
SasSwart Nov 11, 2024
bd6f3ed
handle tag reading error
SasSwart Nov 11, 2024
a2476dc
remove assignment to a nil map
SasSwart Nov 11, 2024
e835326
filter by tags in dbmem
SasSwart Nov 11, 2024
23bd23f
correctly set the default value when no tags are provided
SasSwart Nov 11, 2024
9985ef6
support the zero value tag in sql
SasSwart Nov 11, 2024
a173bfb
update the default value for when no tags are provided
SasSwart Nov 12, 2024
70c7ea9
use a sql domain as a type alias so that we can override it as a Stri…
SasSwart Nov 12, 2024
1c57bd2
complete down migration
SasSwart Nov 12, 2024
ebb716d
complete down migration
SasSwart Nov 12, 2024
96b64f4
update the default value for when no tags are provided
SasSwart Nov 12, 2024
5941dc3
fix job acquisition for untagged provisioners
SasSwart Nov 12, 2024
ff75f5e
make gen
SasSwart Nov 12, 2024
85b1ef4
review notes
SasSwart Nov 12, 2024
071fdc4
fix down migration
SasSwart Nov 12, 2024
78abf67
use the correct name for provisionerdaemons' tag field
SasSwart Nov 12, 2024
d1c7d3e
typo
SasSwart Nov 12, 2024
38d77cf
use the updated tag name
SasSwart Nov 12, 2024
1c64353
fix special case of tagset_contains
SasSwart Nov 12, 2024
8508cd8
Use a stringmap instead of a stringslice for provisioner tags
SasSwart Nov 13, 2024
9dcbb83
update tags param parsing in provisionerDaemons
SasSwart Nov 13, 2024
4c1fc0d
update tags param parsing in provisionerDaemons
SasSwart Nov 13, 2024
fbd70f3
fix special case of tagset_contains
SasSwart Nov 13, 2024
69126f4
rename tagset_contains function
SasSwart Nov 13, 2024
e109caf
attempt to fix the special case for provisioner_tagset_contains
SasSwart Nov 13, 2024
f10561e
attempt to fix the special case for provisioner_tagset_contains
SasSwart Nov 13, 2024
a56b130
attempt to fix the special case for provisioner_tagset_contains
SasSwart Nov 13, 2024
2bce007
fix provisioner_tagset_contains
SasSwart Nov 13, 2024
2860f5d
remove defunct code
SasSwart Nov 14, 2024
d6f26df
Add tag filtering tests for GetProvisionerDaemons
SasSwart Nov 14, 2024
1ce410f
fix foreign key constraint in tests
SasSwart Nov 14, 2024
c065742
Add linting
SasSwart Nov 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -1888,7 +1888,7 @@ func (q *querier) GetProvisionerDaemons(ctx context.Context) ([]database.Provisi
return fetchWithPostFilter(q.auth, policy.ActionRead, fetch)(ctx, nil)
}

func (q *querier) GetProvisionerDaemonsByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerDaemon, error) {
func (q *querier) GetProvisionerDaemonsByOrganization(ctx context.Context, organizationID database.GetProvisionerDaemonsByOrganizationParams) ([]database.ProvisionerDaemon, error) {
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetProvisionerDaemonsByOrganization)(ctx, organizationID)
}

Expand Down
6 changes: 3 additions & 3 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2066,9 +2066,9 @@ func (s *MethodTestSuite) TestExtraMethods() {
}),
})
s.NoError(err, "insert provisioner daemon")
ds, err := db.GetProvisionerDaemonsByOrganization(context.Background(), org.ID)
ds, err := db.GetProvisionerDaemonsByOrganization(context.Background(), database.GetProvisionerDaemonsByOrganizationParams{OrganizationID: org.ID})
s.NoError(err, "get provisioner daemon by org")
check.Args(org.ID).Asserts(d, policy.ActionRead).Returns(ds)
check.Args(database.GetProvisionerDaemonsByOrganizationParams{OrganizationID: org.ID}).Asserts(d, policy.ActionRead).Returns(ds)
}))
s.Run("DeleteOldProvisionerDaemons", s.Subtest(func(db database.Store, check *expects) {
_, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{
Expand Down Expand Up @@ -2560,7 +2560,7 @@ func (s *MethodTestSuite) TestSystemFunctions() {
j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
StartedAt: sql.NullTime{Valid: false},
})
check.Args(database.AcquireProvisionerJobParams{OrganizationID: j.OrganizationID, Types: []database.ProvisionerType{j.Provisioner}, Tags: must(json.Marshal(j.Tags))}).
check.Args(database.AcquireProvisionerJobParams{OrganizationID: j.OrganizationID, Types: []database.ProvisionerType{j.Provisioner}, ProvisionerTags: must(json.Marshal(j.Tags))}).
Asserts( /*rbac.ResourceSystem, policy.ActionUpdate*/ )
}))
s.Run("UpdateProvisionerJobWithCompleteByID", s.Subtest(func(db database.Store, check *expects) {
Expand Down
4 changes: 2 additions & 2 deletions coderd/database/dbfake/dbfake.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,8 @@ func (b WorkspaceBuildBuilder) Do() WorkspaceResponse {
UUID: uuid.New(),
Valid: true,
},
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
Tags: []byte(`{"scope": "organization"}`),
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
ProvisionerTags: []byte(`{"scope": "organization"}`),
})
require.NoError(b.t, err, "acquire starting job")
if j.ID == job.ID {
Expand Down
10 changes: 5 additions & 5 deletions coderd/database/dbgen/dbgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,11 +531,11 @@ func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig data
}
if !orig.StartedAt.Time.IsZero() {
job, err = db.AcquireProvisionerJob(genCtx, database.AcquireProvisionerJobParams{
StartedAt: orig.StartedAt,
OrganizationID: job.OrganizationID,
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
Tags: must(json.Marshal(orig.Tags)),
WorkerID: uuid.NullUUID{},
StartedAt: orig.StartedAt,
OrganizationID: job.OrganizationID,
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
ProvisionerTags: must(json.Marshal(orig.Tags)),
WorkerID: uuid.NullUUID{},
})
require.NoError(t, err)
// There is no easy way to make sure we acquire the correct job.
Expand Down
24 changes: 18 additions & 6 deletions coderd/database/dbmem/dbmem.go
Original file line number Diff line number Diff line change
Expand Up @@ -1194,8 +1194,8 @@ func (q *FakeQuerier) AcquireProvisionerJob(_ context.Context, arg database.Acqu
continue
}
tags := map[string]string{}
if arg.Tags != nil {
err := json.Unmarshal(arg.Tags, &tags)
if arg.ProvisionerTags != nil {
err := json.Unmarshal(arg.ProvisionerTags, &tags)
if err != nil {
return provisionerJob, xerrors.Errorf("unmarshal: %w", err)
}
Expand Down Expand Up @@ -3625,16 +3625,28 @@ func (q *FakeQuerier) GetProvisionerDaemons(_ context.Context) ([]database.Provi
return out, nil
}

func (q *FakeQuerier) GetProvisionerDaemonsByOrganization(_ context.Context, organizationID uuid.UUID) ([]database.ProvisionerDaemon, error) {
func (q *FakeQuerier) GetProvisionerDaemonsByOrganization(_ context.Context, arg database.GetProvisionerDaemonsByOrganizationParams) ([]database.ProvisionerDaemon, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()

daemons := make([]database.ProvisionerDaemon, 0)
for _, daemon := range q.provisionerDaemons {
if daemon.OrganizationID == organizationID {
daemon.Tags = maps.Clone(daemon.Tags)
daemons = append(daemons, daemon)
if daemon.OrganizationID != arg.OrganizationID {
continue
}
// Special case for untagged provisioners: only match untagged jobs.
// Ref: coderd/database/queries/provisionerjobs.sql:24-30
// CASE WHEN nested.tags :: jsonb = '{"scope": "organization", "owner": ""}' :: jsonb
// THEN nested.tags :: jsonb = @tags :: jsonb
if tagsEqual(arg.WantTags, tagsUntagged) && !tagsEqual(arg.WantTags, daemon.Tags) {
continue
}
// ELSE nested.tags :: jsonb <@ @tags :: jsonb
if !tagsSubset(arg.WantTags, daemon.Tags) {
continue
}
daemon.Tags = maps.Clone(daemon.Tags)
daemons = append(daemons, daemon)
}

return daemons, nil
Expand Down
4 changes: 2 additions & 2 deletions coderd/database/dbmetrics/querymetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion coderd/database/dbmock/dbmock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions coderd/database/migrations/000274_check_tags.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DROP FUNCTION IF EXISTS provisioner_tagset_contains(tagset, tagset);

DROP DOMAIN IF EXISTS tagset;
17 changes: 17 additions & 0 deletions coderd/database/migrations/000274_check_tags.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE DOMAIN tagset AS jsonb;

COMMENT ON DOMAIN tagset IS 'A set of tags that match provisioner daemons to provisioner jobs, which can originate from workspaces or templates. tagset is a narrowed type over jsonb. It is expected to be the JSON representation of map[string]string. That is, {"key1": "value1", "key2": "value2"}. We need the narrowed type instead of just using jsonb so that we can give sqlc a type hint, otherwise it defaults to json.RawMessage. json.RawMessage is a suboptimal type to use in the context that we need tagset for.';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ thanks for adding an explanation.
I'm curious why you needed to add an override for this type instead of doing so on a field-level like with

...
          - column: "provisioner_daemons.tags"
            go_type:
              type: "StringMap"
...

for example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been a few days since I've tested this. Happy to retest. But from memory of when I was generating the code for this, the type inference for the column works just fine. It just doesn't transitively apply to the parameters passed into the select query.

I'll have a look into this once more just to be able to give you a more concrete answer at least.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely non-blocking; this can be done after merge


CREATE OR REPLACE FUNCTION provisioner_tagset_contains(provisioner_tags tagset, job_tags tagset)
RETURNS boolean AS $$
BEGIN
RETURN CASE
-- Special case for untagged provisioners, where only an exact match should count
WHEN job_tags::jsonb = '{"scope": "organization", "owner": ""}'::jsonb THEN job_tags::jsonb = provisioner_tags::jsonb
-- General case
ELSE job_tags::jsonb <@ provisioner_tags::jsonb
END;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION provisioner_tagset_contains(tagset, tagset) IS 'Returns true if the provisioner_tags contains the job_tags, or if the job_tags represents an untagged provisioner and the superset is exactly equal to the subset.';
2 changes: 1 addition & 1 deletion coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion coderd/database/querier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1020,7 +1020,7 @@ func TestQueuePosition(t *testing.T) {
UUID: uuid.New(),
Valid: true,
},
Tags: json.RawMessage("{}"),
ProvisionerTags: json.RawMessage("{}"),
})
require.NoError(t, err)
require.Equal(t, jobs[0].ID, job.ID)
Expand Down
47 changes: 26 additions & 21 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion coderd/database/queries/provisionerdaemons.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ SELECT
FROM
provisioner_daemons
WHERE
organization_id = @organization_id;
-- This is the original search criteria:
organization_id = @organization_id :: uuid
AND
-- adding support for searching by tags:
(@want_tags :: tagset = 'null' :: tagset OR provisioner_tagset_contains(provisioner_daemons.tags::tagset, @want_tags::tagset));

-- name: DeleteOldProvisionerDaemons :exec
-- Delete provisioner daemons that have been created at least a week ago
Expand Down
22 changes: 9 additions & 13 deletions coderd/database/queries/provisionerjobs.sql
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,17 @@ WHERE
SELECT
id
FROM
provisioner_jobs AS nested
provisioner_jobs AS potential_job
WHERE
nested.started_at IS NULL
AND nested.organization_id = @organization_id
potential_job.started_at IS NULL
AND potential_job.organization_id = @organization_id
-- Ensure the caller has the correct provisioner.
AND nested.provisioner = ANY(@types :: provisioner_type [ ])
AND CASE
-- Special case for untagged provisioners: only match untagged jobs.
WHEN nested.tags :: jsonb = '{"scope": "organization", "owner": ""}' :: jsonb
THEN nested.tags :: jsonb = @tags :: jsonb
-- Ensure the caller satisfies all job tags.
ELSE nested.tags :: jsonb <@ @tags :: jsonb
END
AND potential_job.provisioner = ANY(@types :: provisioner_type [ ])
-- elsewhere, we use the tagset type, but here we use jsonb for backward compatibility
-- they are aliases and the code that calls this query already relies on a different type
AND provisioner_tagset_contains(@provisioner_tags :: jsonb, potential_job.tags :: jsonb)
ORDER BY
nested.created_at
potential_job.created_at
FOR UPDATE
SKIP LOCKED
LIMIT
Expand Down Expand Up @@ -160,4 +156,4 @@ RETURNING *;
-- name: GetProvisionerJobTimingsByJobID :many
SELECT * FROM provisioner_job_timings
WHERE job_id = $1
ORDER BY started_at ASC;
ORDER BY started_at ASC;
3 changes: 3 additions & 0 deletions coderd/database/sqlc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ sql:
- db_type: "name_organization_pair"
go_type:
type: "NameOrganizationPair"
- db_type: "tagset"
go_type:
type: "StringMap"
- column: "custom_roles.site_permissions"
go_type:
type: "CustomRolePermissions"
Expand Down
4 changes: 2 additions & 2 deletions coderd/provisionerdserver/acquirer.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ func (a *Acquirer) AcquireJob(
UUID: worker,
Valid: true,
},
Types: pt,
Tags: dbTags,
Types: pt,
ProvisionerTags: dbTags,
})
if xerrors.Is(err, sql.ErrNoRows) {
logger.Debug(ctx, "no job available")
Expand Down
2 changes: 1 addition & 1 deletion coderd/provisionerdserver/acquirer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ func (s *fakeTaggedStore) AcquireProvisionerJob(
) {
defer func() { s.params <- params }()
var tags provisionerdserver.Tags
err := json.Unmarshal(params.Tags, &tags)
err := json.Unmarshal(params.ProvisionerTags, &tags)
if !assert.NoError(s.t, err) {
return database.ProvisionerJob{}, err
}
Expand Down
Loading
Loading