mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
feat(coderd): update API to allow filtering provisioner daemons by tags (#15448)
This PR provides new parameters to an endpoint that will be necessary for #15048
This commit is contained in:
@ -56,13 +56,33 @@ func (api *API) provisionerDaemonsEnabledMW(next http.Handler) http.Handler {
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Param organization path string true "Organization ID" format(uuid)
|
||||
// @Param tags query object false "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})"
|
||||
// @Success 200 {array} codersdk.ProvisionerDaemon
|
||||
// @Router /organizations/{organization}/provisionerdaemons [get]
|
||||
func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
org := httpmw.OrganizationParam(r)
|
||||
var (
|
||||
ctx = r.Context()
|
||||
org = httpmw.OrganizationParam(r)
|
||||
tagParam = r.URL.Query().Get("tags")
|
||||
tags = database.StringMap{}
|
||||
err = tags.Scan([]byte(tagParam))
|
||||
)
|
||||
|
||||
daemons, err := api.Database.GetProvisionerDaemonsByOrganization(ctx, org.ID)
|
||||
if tagParam != "" && err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid tags query parameter",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
daemons, err := api.Database.GetProvisionerDaemonsByOrganization(
|
||||
ctx,
|
||||
database.GetProvisionerDaemonsByOrganizationParams{
|
||||
OrganizationID: org.ID,
|
||||
WantTags: tags,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching provisioner daemons.",
|
||||
|
@ -3,10 +3,12 @@ package coderd_test
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -18,6 +20,7 @@ import (
|
||||
"github.com/coder/coder/v2/buildinfo"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/provisionerkey"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
@ -768,7 +771,7 @@ func TestGetProvisionerDaemons(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
srv.DRPCConn().Close()
|
||||
|
||||
daemons, err := orgAdmin.OrganizationProvisionerDaemons(ctx, org.ID)
|
||||
daemons, err := orgAdmin.OrganizationProvisionerDaemons(ctx, org.ID, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 1)
|
||||
|
||||
@ -794,4 +797,207 @@ func TestGetProvisionerDaemons(t *testing.T) {
|
||||
_, err = outsideOrg.ListProvisionerKeyDaemons(ctx, org.ID)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("filtered by tags", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
tagsToFilterBy map[string]string
|
||||
provisionerDaemonTags map[string]string
|
||||
expectToGetDaemon bool
|
||||
}{
|
||||
{
|
||||
name: "only an empty tagset finds an untagged provisioner",
|
||||
tagsToFilterBy: map[string]string{"scope": "organization", "owner": ""},
|
||||
provisionerDaemonTags: map[string]string{"scope": "organization", "owner": ""},
|
||||
expectToGetDaemon: true,
|
||||
},
|
||||
{
|
||||
name: "an exact match with a single optional tag finds a provisioner daemon",
|
||||
tagsToFilterBy: map[string]string{"scope": "organization", "owner": "", "environment": "on-prem"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "organization", "owner": "", "environment": "on-prem"},
|
||||
expectToGetDaemon: true,
|
||||
},
|
||||
{
|
||||
name: "a subset of filter tags finds a daemon with a superset of tags",
|
||||
tagsToFilterBy: map[string]string{"scope": "organization", "owner": "", "environment": "on-prem"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "organization", "owner": "", "environment": "on-prem", "datacenter": "chicago"},
|
||||
expectToGetDaemon: true,
|
||||
},
|
||||
{
|
||||
name: "an exact match with two additional tags finds a provisioner daemon",
|
||||
tagsToFilterBy: map[string]string{"scope": "organization", "owner": "", "environment": "on-prem", "datacenter": "chicago"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "organization", "owner": "", "environment": "on-prem", "datacenter": "chicago"},
|
||||
expectToGetDaemon: true,
|
||||
},
|
||||
{
|
||||
name: "a user scoped filter tag set finds a user scoped provisioner daemon",
|
||||
tagsToFilterBy: map[string]string{"scope": "user", "owner": "aaa"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "user", "owner": "aaa"},
|
||||
expectToGetDaemon: true,
|
||||
},
|
||||
{
|
||||
name: "a user scoped filter tag set finds a user scoped provisioner daemon with an additional tag",
|
||||
tagsToFilterBy: map[string]string{"scope": "user", "owner": "aaa"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "user", "owner": "aaa", "environment": "on-prem"},
|
||||
expectToGetDaemon: true,
|
||||
},
|
||||
{
|
||||
name: "user-scoped provisioner with tags and user-scoped filter with tags",
|
||||
tagsToFilterBy: map[string]string{"scope": "user", "owner": "aaa", "environment": "on-prem"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "user", "owner": "aaa", "environment": "on-prem"},
|
||||
expectToGetDaemon: true,
|
||||
},
|
||||
{
|
||||
name: "user-scoped provisioner with multiple tags and user-scoped filter with a subset of tags",
|
||||
tagsToFilterBy: map[string]string{"scope": "user", "owner": "aaa", "environment": "on-prem"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "user", "owner": "aaa", "environment": "on-prem", "datacenter": "chicago"},
|
||||
expectToGetDaemon: true,
|
||||
},
|
||||
{
|
||||
name: "user-scoped provisioner with multiple tags and user-scoped filter with multiple tags",
|
||||
tagsToFilterBy: map[string]string{"scope": "user", "owner": "aaa", "environment": "on-prem", "datacenter": "chicago"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "user", "owner": "aaa", "environment": "on-prem", "datacenter": "chicago"},
|
||||
expectToGetDaemon: true,
|
||||
},
|
||||
{
|
||||
name: "untagged provisioner and tagged filter",
|
||||
tagsToFilterBy: map[string]string{"scope": "organization", "owner": "", "environment": "on-prem"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "organization", "owner": ""},
|
||||
expectToGetDaemon: false,
|
||||
},
|
||||
{
|
||||
name: "tagged provisioner and untagged filter",
|
||||
tagsToFilterBy: map[string]string{"scope": "organization", "owner": ""},
|
||||
provisionerDaemonTags: map[string]string{"scope": "organization", "owner": "", "environment": "on-prem"},
|
||||
expectToGetDaemon: false,
|
||||
},
|
||||
{
|
||||
name: "tagged provisioner and double-tagged filter",
|
||||
tagsToFilterBy: map[string]string{"scope": "organization", "owner": "", "environment": "on-prem", "datacenter": "chicago"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "organization", "owner": "", "environment": "on-prem"},
|
||||
expectToGetDaemon: false,
|
||||
},
|
||||
{
|
||||
name: "double-tagged provisioner and double-tagged filter with differing tags",
|
||||
tagsToFilterBy: map[string]string{"scope": "organization", "owner": "", "environment": "on-prem", "datacenter": "chicago"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "organization", "owner": "", "environment": "on-prem", "datacenter": "new_york"},
|
||||
expectToGetDaemon: false,
|
||||
},
|
||||
{
|
||||
name: "user-scoped provisioner and untagged filter",
|
||||
tagsToFilterBy: map[string]string{"scope": "organization", "owner": ""},
|
||||
provisionerDaemonTags: map[string]string{"scope": "user", "owner": "aaa"},
|
||||
expectToGetDaemon: false,
|
||||
},
|
||||
{
|
||||
name: "user-scoped provisioner and different user-scoped filter",
|
||||
tagsToFilterBy: map[string]string{"scope": "user", "owner": "bbb"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "user", "owner": "aaa"},
|
||||
expectToGetDaemon: false,
|
||||
},
|
||||
{
|
||||
name: "org-scoped provisioner and user-scoped filter",
|
||||
tagsToFilterBy: map[string]string{"scope": "user", "owner": "aaa"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "organization", "owner": ""},
|
||||
expectToGetDaemon: false,
|
||||
},
|
||||
{
|
||||
name: "user-scoped provisioner and org-scoped filter with tags",
|
||||
tagsToFilterBy: map[string]string{"scope": "user", "owner": "aaa", "environment": "on-prem"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "organization", "owner": ""},
|
||||
expectToGetDaemon: false,
|
||||
},
|
||||
{
|
||||
name: "user-scoped provisioner and user-scoped filter with tags",
|
||||
tagsToFilterBy: map[string]string{"scope": "user", "owner": "aaa", "environment": "on-prem"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "user", "owner": "aaa"},
|
||||
expectToGetDaemon: false,
|
||||
},
|
||||
{
|
||||
name: "user-scoped provisioner with tags and user-scoped filter with multiple tags",
|
||||
tagsToFilterBy: map[string]string{"scope": "user", "owner": "aaa", "environment": "on-prem", "datacenter": "chicago"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "user", "owner": "aaa", "environment": "on-prem"},
|
||||
expectToGetDaemon: false,
|
||||
},
|
||||
{
|
||||
name: "user-scoped provisioner with tags and user-scoped filter with differing tags",
|
||||
tagsToFilterBy: map[string]string{"scope": "user", "owner": "aaa", "environment": "on-prem", "datacenter": "new_york"},
|
||||
provisionerDaemonTags: map[string]string{"scope": "user", "owner": "aaa", "environment": "on-prem", "datacenter": "chicago"},
|
||||
expectToGetDaemon: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
client, db, _ := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
ProvisionerDaemonPSK: "provisionersftw",
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureExternalProvisionerDaemons: 1,
|
||||
codersdk.FeatureMultipleOrganizations: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
org := coderdenttest.CreateOrganization(t, client, coderdenttest.CreateOrganizationOptions{
|
||||
IncludeProvisionerDaemon: false,
|
||||
})
|
||||
orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, org.ID, rbac.ScopedRoleOrgMember(org.ID))
|
||||
|
||||
daemonCreatedAt := time.Now()
|
||||
|
||||
//nolint:gocritic // We're not testing auth on the following in this test
|
||||
provisionerKey, err := db.InsertProvisionerKey(dbauthz.AsSystemRestricted(ctx), database.InsertProvisionerKeyParams{
|
||||
Name: "Test Provisioner Key",
|
||||
ID: uuid.New(),
|
||||
CreatedAt: daemonCreatedAt,
|
||||
OrganizationID: org.ID,
|
||||
HashedSecret: []byte{},
|
||||
Tags: tt.provisionerDaemonTags,
|
||||
})
|
||||
require.NoError(t, err, "should be able to create a provisioner key")
|
||||
|
||||
//nolint:gocritic // We're not testing auth on the following in this test
|
||||
pd, err := db.UpsertProvisionerDaemon(dbauthz.AsSystemRestricted(ctx), database.UpsertProvisionerDaemonParams{
|
||||
CreatedAt: daemonCreatedAt,
|
||||
Name: "Test Provisioner Daemon",
|
||||
Provisioners: []database.ProvisionerType{},
|
||||
Tags: tt.provisionerDaemonTags,
|
||||
LastSeenAt: sql.NullTime{
|
||||
Time: daemonCreatedAt,
|
||||
Valid: true,
|
||||
},
|
||||
Version: "",
|
||||
OrganizationID: org.ID,
|
||||
APIVersion: "",
|
||||
KeyID: provisionerKey.ID,
|
||||
})
|
||||
require.NoError(t, err, "should be able to create provisioner daemon")
|
||||
daemonAsCreated := db2sdk.ProvisionerDaemon(pd)
|
||||
|
||||
allDaemons, err := orgAdmin.OrganizationProvisionerDaemons(ctx, org.ID, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, allDaemons, 1)
|
||||
|
||||
daemonsAsFound, err := orgAdmin.OrganizationProvisionerDaemons(ctx, org.ID, tt.tagsToFilterBy)
|
||||
if tt.expectToGetDaemon {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemonsAsFound, 1)
|
||||
require.Equal(t, daemonAsCreated.Tags, daemonsAsFound[0].Tags, "found daemon should have the same tags as created daemon")
|
||||
require.Equal(t, daemonsAsFound[0].KeyID, provisionerKey.ID)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, daemonsAsFound, "should not have found daemon")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ func (api *API) provisionerKeyDaemons(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
sdkKeys := convertProvisionerKeys(pks)
|
||||
|
||||
daemons, err := api.Database.GetProvisionerDaemonsByOrganization(ctx, organization.ID)
|
||||
daemons, err := api.Database.GetProvisionerDaemonsByOrganization(ctx, database.GetProvisionerDaemonsByOrganizationParams{OrganizationID: organization.ID})
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
|
@ -250,8 +250,8 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
||||
UUID: uuid.New(),
|
||||
Valid: true,
|
||||
},
|
||||
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
|
||||
Tags: json.RawMessage(fmt.Sprintf(`{%q: "yeah"}`, c.name)),
|
||||
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
|
||||
ProvisionerTags: json.RawMessage(fmt.Sprintf(`{%q: "yeah"}`, c.name)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, job.ID, acquiredJob.ID)
|
||||
@ -534,8 +534,8 @@ func TestTemplateUpdateBuildDeadlinesSkip(t *testing.T) {
|
||||
UUID: uuid.New(),
|
||||
Valid: true,
|
||||
},
|
||||
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
|
||||
Tags: json.RawMessage(fmt.Sprintf(`{%q: "yeah"}`, wsID)),
|
||||
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
|
||||
ProvisionerTags: json.RawMessage(fmt.Sprintf(`{%q: "yeah"}`, wsID)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, job.ID, acquiredJob.ID)
|
||||
|
Reference in New Issue
Block a user