mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: add agents_allowed to templates (#27284)
Relates to CODAGT-713 This adds `templates.agents_allowed` as a default-true, auditable template attribute, along with nullable database filtering. Migration `000562` translates the effective legacy `agents_template_allowlist` state for existing templates: a valid nonempty list allows matching templates and blocks the rest, missing or empty values leave templates allowed, whilst corrupt values fail closed by blocking all existing templates. As per the linear issue, new templates deliberately default to allowed under the per-template model. This is the database-only first PR in the stack. #27285 makes the field authoritative in the API and chatd whilst temporarily retaining the compatibility routes needed by the shipped frontend. Later PRs migrate the UI, remove the legacy storage, routes, SDK types, and utility, then add CLI flags.
This commit is contained in:
@@ -555,6 +555,7 @@ func Template(t testing.TB, db database.Store, seed database.Template) database.
|
||||
MaxPortSharingLevel: takeFirst(seed.MaxPortSharingLevel, database.AppSharingLevelOwner),
|
||||
UseClassicParameterFlow: takeFirst(seed.UseClassicParameterFlow, false),
|
||||
CorsBehavior: takeFirst(seed.CorsBehavior, database.CorsBehaviorSimple),
|
||||
AgentsAllowed: seed.AgentsAllowed,
|
||||
})
|
||||
require.NoError(t, err, "insert template")
|
||||
|
||||
|
||||
Generated
+5
-1
@@ -3461,7 +3461,8 @@ CREATE TABLE templates (
|
||||
use_classic_parameter_flow boolean DEFAULT false NOT NULL,
|
||||
cors_behavior cors_behavior DEFAULT 'simple'::cors_behavior NOT NULL,
|
||||
disable_module_cache boolean DEFAULT false NOT NULL,
|
||||
time_til_autostop_notify bigint DEFAULT 0 NOT NULL
|
||||
time_til_autostop_notify bigint DEFAULT 0 NOT NULL,
|
||||
agents_allowed boolean DEFAULT true NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.';
|
||||
@@ -3486,6 +3487,8 @@ COMMENT ON COLUMN templates.use_classic_parameter_flow IS 'Determines whether to
|
||||
|
||||
COMMENT ON COLUMN templates.time_til_autostop_notify IS 'How long before the workspace autostop deadline to send a reminder notification, in nanoseconds. 0 disables the notification.';
|
||||
|
||||
COMMENT ON COLUMN templates.agents_allowed IS 'Whether Coder Agents can create workspaces using this template.';
|
||||
|
||||
CREATE VIEW template_with_names AS
|
||||
SELECT templates.id,
|
||||
templates.created_at,
|
||||
@@ -3519,6 +3522,7 @@ CREATE VIEW template_with_names AS
|
||||
templates.cors_behavior,
|
||||
templates.disable_module_cache,
|
||||
templates.time_til_autostop_notify,
|
||||
templates.agents_allowed,
|
||||
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
|
||||
COALESCE(visible_users.username, ''::text) AS created_by_username,
|
||||
COALESCE(visible_users.name, ''::text) AS created_by_name,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
DROP VIEW template_with_names;
|
||||
|
||||
ALTER TABLE templates DROP COLUMN agents_allowed;
|
||||
|
||||
CREATE VIEW template_with_names AS
|
||||
SELECT templates.*,
|
||||
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
|
||||
COALESCE(visible_users.username, ''::text) AS created_by_username,
|
||||
COALESCE(visible_users.name, ''::text) AS created_by_name,
|
||||
COALESCE(organizations.name, ''::text) AS organization_name,
|
||||
COALESCE(organizations.display_name, ''::text) AS organization_display_name,
|
||||
COALESCE(organizations.icon, ''::text) AS organization_icon
|
||||
FROM ((templates
|
||||
LEFT JOIN visible_users ON ((templates.created_by = visible_users.id)))
|
||||
LEFT JOIN organizations ON ((templates.organization_id = organizations.id)));
|
||||
|
||||
COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.';
|
||||
@@ -0,0 +1,64 @@
|
||||
ALTER TABLE templates ADD COLUMN agents_allowed boolean DEFAULT true NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN templates.agents_allowed IS 'Whether Coder Agents can create workspaces using this template.';
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
raw text;
|
||||
parsed jsonb;
|
||||
parsed_ids uuid[];
|
||||
BEGIN
|
||||
SELECT value INTO raw
|
||||
FROM site_configs
|
||||
WHERE key = 'agents_template_allowlist';
|
||||
|
||||
IF raw IS NULL OR btrim(raw) = '' THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
BEGIN
|
||||
parsed := raw::jsonb;
|
||||
IF parsed = 'null'::jsonb THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
IF jsonb_typeof(parsed) <> 'array' THEN
|
||||
RAISE EXCEPTION 'value is not a JSON array';
|
||||
END IF;
|
||||
IF jsonb_array_length(parsed) = 0 THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
SELECT array_agg(entry::uuid)
|
||||
INTO parsed_ids
|
||||
FROM jsonb_array_elements_text(parsed) AS entries(entry);
|
||||
|
||||
IF array_position(parsed_ids, NULL) IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'contains a null template ID';
|
||||
END IF;
|
||||
EXCEPTION WHEN others THEN
|
||||
RAISE WARNING 'agents_template_allowlist is corrupt (%); blocking all templates', SQLERRM;
|
||||
parsed_ids := ARRAY[]::uuid[];
|
||||
END;
|
||||
|
||||
-- A valid nonempty list allows matching existing templates only. Missing, null,
|
||||
-- or empty data leaves templates allowed. Corrupt data blocks all templates.
|
||||
UPDATE templates
|
||||
SET agents_allowed = (id = ANY(parsed_ids));
|
||||
END $$;
|
||||
|
||||
-- As usual, recreate the view so templates.* is expanded to include the new column.
|
||||
DROP VIEW template_with_names;
|
||||
|
||||
CREATE VIEW template_with_names AS
|
||||
SELECT templates.*,
|
||||
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
|
||||
COALESCE(visible_users.username, ''::text) AS created_by_username,
|
||||
COALESCE(visible_users.name, ''::text) AS created_by_name,
|
||||
COALESCE(organizations.name, ''::text) AS organization_name,
|
||||
COALESCE(organizations.display_name, ''::text) AS organization_display_name,
|
||||
COALESCE(organizations.icon, ''::text) AS organization_icon
|
||||
FROM ((templates
|
||||
LEFT JOIN visible_users ON ((templates.created_by = visible_users.id)))
|
||||
LEFT JOIN organizations ON ((templates.organization_id = organizations.id)));
|
||||
|
||||
COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.';
|
||||
@@ -1865,6 +1865,244 @@ func TestMigration000558AuditOAuth2ProviderSettingsEnumInSingleTxn(t *testing.T)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
//nolint:tparallel,paralleltest // Subtests share one database and exercise sequential migration state.
|
||||
func TestMigration000563TemplateAgentsAllowedBackfill(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqlDB, ctx, orgID, userID, templateIDs := setupMigration000563Templates(t)
|
||||
upSQL, err := os.ReadFile("000563_template_agents_allowed.up.sql")
|
||||
require.NoError(t, err)
|
||||
downSQL, err := os.ReadFile("000563_template_agents_allowed.down.sql")
|
||||
require.NoError(t, err)
|
||||
|
||||
staleID := uuid.New()
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
present bool
|
||||
checkPostMigrationDefault bool
|
||||
want map[uuid.UUID]bool
|
||||
}{
|
||||
{
|
||||
name: "valid nonempty list",
|
||||
value: fmt.Sprintf(`[%q]`, templateIDs[0]),
|
||||
present: true,
|
||||
checkPostMigrationDefault: true,
|
||||
want: map[uuid.UUID]bool{
|
||||
templateIDs[0]: true,
|
||||
templateIDs[1]: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stale template ID",
|
||||
value: fmt.Sprintf(`[%q,%q]`, templateIDs[0], staleID),
|
||||
present: true,
|
||||
want: map[uuid.UUID]bool{
|
||||
templateIDs[0]: true,
|
||||
templateIDs[1]: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing",
|
||||
want: map[uuid.UUID]bool{
|
||||
templateIDs[0]: true,
|
||||
templateIDs[1]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
value: "",
|
||||
present: true,
|
||||
want: map[uuid.UUID]bool{
|
||||
templateIDs[0]: true,
|
||||
templateIDs[1]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "JSON null",
|
||||
value: "null",
|
||||
present: true,
|
||||
want: map[uuid.UUID]bool{
|
||||
templateIDs[0]: true,
|
||||
templateIDs[1]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid JSON",
|
||||
value: "{",
|
||||
present: true,
|
||||
want: map[uuid.UUID]bool{
|
||||
templateIDs[0]: false,
|
||||
templateIDs[1]: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "JSON object",
|
||||
value: `{}`,
|
||||
present: true,
|
||||
want: map[uuid.UUID]bool{
|
||||
templateIDs[0]: false,
|
||||
templateIDs[1]: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "JSON scalar",
|
||||
value: `"value"`,
|
||||
present: true,
|
||||
want: map[uuid.UUID]bool{
|
||||
templateIDs[0]: false,
|
||||
templateIDs[1]: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid UUID element",
|
||||
value: `["not-a-uuid"]`,
|
||||
present: true,
|
||||
want: map[uuid.UUID]bool{
|
||||
templateIDs[0]: false,
|
||||
templateIDs[1]: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "null element",
|
||||
value: `[null]`,
|
||||
present: true,
|
||||
want: map[uuid.UUID]bool{
|
||||
templateIDs[0]: false,
|
||||
templateIDs[1]: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed valid and invalid elements",
|
||||
value: fmt.Sprintf(`[%q,"not-a-uuid"]`, templateIDs[0]),
|
||||
present: true,
|
||||
want: map[uuid.UUID]bool{
|
||||
templateIDs[0]: false,
|
||||
templateIDs[1]: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty array",
|
||||
value: "[]",
|
||||
present: true,
|
||||
want: map[uuid.UUID]bool{
|
||||
templateIDs[0]: true,
|
||||
templateIDs[1]: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := sqlDB.ExecContext(ctx, `DELETE FROM site_configs WHERE key = 'agents_template_allowlist'`)
|
||||
require.NoError(t, err)
|
||||
if tt.present {
|
||||
_, err = sqlDB.ExecContext(ctx, `INSERT INTO site_configs (key, value) VALUES ('agents_template_allowlist', $1)`, tt.value)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err = sqlDB.ExecContext(ctx, string(upSQL))
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_, err := sqlDB.ExecContext(ctx, string(downSQL))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
rows, err := sqlDB.QueryContext(ctx, `SELECT id, agents_allowed FROM templates`)
|
||||
require.NoError(t, err)
|
||||
got := make(map[uuid.UUID]bool, len(templateIDs))
|
||||
for rows.Next() {
|
||||
var id uuid.UUID
|
||||
var agentsAllowed bool
|
||||
require.NoError(t, rows.Scan(&id, &agentsAllowed))
|
||||
got[id] = agentsAllowed
|
||||
}
|
||||
require.NoError(t, rows.Close())
|
||||
require.NoError(t, rows.Err())
|
||||
require.Equal(t, tt.want, got)
|
||||
|
||||
var stored string
|
||||
err = sqlDB.QueryRowContext(ctx, `SELECT value FROM site_configs WHERE key = 'agents_template_allowlist'`).Scan(&stored)
|
||||
if tt.present {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.value, stored)
|
||||
} else {
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
}
|
||||
|
||||
if tt.checkPostMigrationDefault {
|
||||
newTemplateID := uuid.New()
|
||||
_, err = sqlDB.ExecContext(ctx, `
|
||||
INSERT INTO templates (id, organization_id, name, created_at, updated_at, provisioner, active_version_id, created_by)
|
||||
VALUES ($1, $2, $3, NOW(), NOW(), 'terraform', $4, $5)
|
||||
`, newTemplateID, orgID, "post-migration-template", uuid.New(), userID)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_, err := sqlDB.ExecContext(ctx, `DELETE FROM templates WHERE id = $1`, newTemplateID)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
var agentsAllowed bool
|
||||
err = sqlDB.QueryRowContext(ctx, `SELECT agents_allowed FROM template_with_names WHERE id = $1`, newTemplateID).Scan(&agentsAllowed)
|
||||
require.NoError(t, err)
|
||||
require.True(t, agentsAllowed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupMigration000563Templates(t *testing.T) (
|
||||
sqlDB *sql.DB,
|
||||
ctx context.Context,
|
||||
orgID uuid.UUID,
|
||||
userID uuid.UUID,
|
||||
templateIDs []uuid.UUID,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
const migrationVersion = 562
|
||||
|
||||
sqlDB = testSQLDB(t)
|
||||
next, err := migrations.Stepper(sqlDB)
|
||||
require.NoError(t, err)
|
||||
for {
|
||||
version, more, err := next()
|
||||
require.NoError(t, err)
|
||||
if !more {
|
||||
t.Fatalf("migration %d not found", migrationVersion)
|
||||
}
|
||||
if version == migrationVersion-1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ctx = testutil.Context(t, testutil.WaitSuperLong)
|
||||
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||
orgID = uuid.New()
|
||||
userID = uuid.New()
|
||||
templateIDs = []uuid.UUID{uuid.New(), uuid.New()}
|
||||
|
||||
_, err = sqlDB.ExecContext(ctx, `
|
||||
INSERT INTO organizations (id, name, display_name, description, created_at, updated_at, default_org_member_roles)
|
||||
VALUES ($1, $2, $3, $4, $5, $5, '{}')
|
||||
`, orgID, "agents-allowed-org", "Agents Allowed Org", "Migration test", now)
|
||||
require.NoError(t, err)
|
||||
_, err = sqlDB.ExecContext(ctx, `
|
||||
INSERT INTO users (id, username, email, hashed_password, created_at, updated_at, status, rbac_roles, login_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $5, 'active', '{}', 'password')
|
||||
`, userID, "agents-allowed-user", "agents-allowed@example.com", []byte{}, now)
|
||||
require.NoError(t, err)
|
||||
for i, templateID := range templateIDs {
|
||||
_, err = sqlDB.ExecContext(ctx, `
|
||||
INSERT INTO templates (id, organization_id, name, created_at, updated_at, provisioner, active_version_id, created_by)
|
||||
VALUES ($1, $2, $3, $4, $4, 'terraform', $5, $6)
|
||||
`, templateID, orgID, fmt.Sprintf("agents-allowed-template-%d", i), now, uuid.New(), userID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return sqlDB, ctx, orgID, userID, templateIDs
|
||||
}
|
||||
|
||||
func TestMigration000498SoftDeleteStaleWorkspaceAgents(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
|
||||
pq.Array(arg.IDs),
|
||||
arg.Deprecated,
|
||||
arg.HasAITask,
|
||||
arg.AgentsAllowed,
|
||||
arg.AuthorID,
|
||||
arg.AuthorUsername,
|
||||
arg.HasExternalAgent,
|
||||
@@ -130,6 +131,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
|
||||
&i.CorsBehavior,
|
||||
&i.DisableModuleCache,
|
||||
&i.TimeTilAutostopNotify,
|
||||
&i.AgentsAllowed,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
&i.CreatedByName,
|
||||
|
||||
Generated
+3
@@ -5910,6 +5910,7 @@ type Template struct {
|
||||
CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"`
|
||||
DisableModuleCache bool `db:"disable_module_cache" json:"disable_module_cache"`
|
||||
TimeTilAutostopNotify int64 `db:"time_til_autostop_notify" json:"time_til_autostop_notify"`
|
||||
AgentsAllowed bool `db:"agents_allowed" json:"agents_allowed"`
|
||||
CreatedByAvatarURL string `db:"created_by_avatar_url" json:"created_by_avatar_url"`
|
||||
CreatedByUsername string `db:"created_by_username" json:"created_by_username"`
|
||||
CreatedByName string `db:"created_by_name" json:"created_by_name"`
|
||||
@@ -5962,6 +5963,8 @@ type TemplateTable struct {
|
||||
DisableModuleCache bool `db:"disable_module_cache" json:"disable_module_cache"`
|
||||
// How long before the workspace autostop deadline to send a reminder notification, in nanoseconds. 0 disables the notification.
|
||||
TimeTilAutostopNotify int64 `db:"time_til_autostop_notify" json:"time_til_autostop_notify"`
|
||||
// Whether Coder Agents can create workspaces using this template.
|
||||
AgentsAllowed bool `db:"agents_allowed" json:"agents_allowed"`
|
||||
}
|
||||
|
||||
// Records aggregated usage statistics for templates/users. All usage is rounded up to the nearest minute.
|
||||
|
||||
@@ -890,6 +890,94 @@ func TestGetWorkspaceAgentUsageStats(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:tparallel,paralleltest // Subtests share one database seeded by the parent test.
|
||||
func TestGetTemplatesWithAgentsAllowedFilter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
allowed := dbgen.Template(t, db, database.Template{
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
AgentsAllowed: true,
|
||||
})
|
||||
require.True(t, allowed.AgentsAllowed)
|
||||
blocked := dbgen.Template(t, db, database.Template{
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
AgentsAllowed: false,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
value sql.NullBool
|
||||
want []uuid.UUID
|
||||
}{
|
||||
{
|
||||
name: "unset",
|
||||
want: []uuid.UUID{allowed.ID, blocked.ID},
|
||||
},
|
||||
{
|
||||
name: "allowed",
|
||||
value: sql.NullBool{Bool: true, Valid: true},
|
||||
want: []uuid.UUID{allowed.ID},
|
||||
},
|
||||
{
|
||||
name: "blocked",
|
||||
value: sql.NullBool{Bool: false, Valid: true},
|
||||
want: []uuid.UUID{blocked.ID},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := db.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{
|
||||
Deleted: false,
|
||||
OrganizationID: org.ID,
|
||||
AgentsAllowed: tt.value,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
gotIDs := make([]uuid.UUID, 0, len(got))
|
||||
for _, template := range got {
|
||||
gotIDs = append(gotIDs, template.ID)
|
||||
}
|
||||
require.ElementsMatch(t, tt.want, gotIDs)
|
||||
})
|
||||
}
|
||||
|
||||
byID, err := db.GetTemplateByID(ctx, blocked.ID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, byID.AgentsAllowed)
|
||||
|
||||
all, err := db.GetTemplates(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, all, 2)
|
||||
for _, template := range all {
|
||||
if template.ID == blocked.ID {
|
||||
require.False(t, template.AgentsAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
prepared, err := (&coderdtest.FakeAuthorizer{}).Prepare(
|
||||
ctx,
|
||||
rbac.Subject{},
|
||||
policy.ActionRead,
|
||||
rbac.ResourceTemplate.Type,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
authorized, err := db.GetAuthorizedTemplates(ctx, database.GetTemplatesWithFilterParams{
|
||||
Deleted: false,
|
||||
OrganizationID: org.ID,
|
||||
AgentsAllowed: sql.NullBool{Bool: false, Valid: true},
|
||||
}, prepared)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, authorized, 1)
|
||||
require.Equal(t, blocked.ID, authorized[0].ID)
|
||||
require.False(t, authorized[0].AgentsAllowed)
|
||||
}
|
||||
|
||||
func TestGetWorkspaceAgentUsageStatsAndLabels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Generated
+32
-14
@@ -26718,7 +26718,7 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, templateID
|
||||
|
||||
const getTemplateByID = `-- name: GetTemplateByID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, agents_allowed, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
|
||||
FROM
|
||||
template_with_names
|
||||
WHERE
|
||||
@@ -26763,6 +26763,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
||||
&i.CorsBehavior,
|
||||
&i.DisableModuleCache,
|
||||
&i.TimeTilAutostopNotify,
|
||||
&i.AgentsAllowed,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
&i.CreatedByName,
|
||||
@@ -26775,7 +26776,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
||||
|
||||
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, agents_allowed, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
|
||||
FROM
|
||||
template_with_names AS templates
|
||||
WHERE
|
||||
@@ -26828,6 +26829,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
||||
&i.CorsBehavior,
|
||||
&i.DisableModuleCache,
|
||||
&i.TimeTilAutostopNotify,
|
||||
&i.AgentsAllowed,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
&i.CreatedByName,
|
||||
@@ -26839,7 +26841,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
||||
}
|
||||
|
||||
const getTemplates = `-- name: GetTemplates :many
|
||||
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates
|
||||
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, agents_allowed, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates
|
||||
ORDER BY (name, id) ASC
|
||||
`
|
||||
|
||||
@@ -26885,6 +26887,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
||||
&i.CorsBehavior,
|
||||
&i.DisableModuleCache,
|
||||
&i.TimeTilAutostopNotify,
|
||||
&i.AgentsAllowed,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
&i.CreatedByName,
|
||||
@@ -26907,7 +26910,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
||||
|
||||
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
|
||||
SELECT
|
||||
t.id, t.created_at, t.updated_at, t.organization_id, t.deleted, t.name, t.provisioner, t.active_version_id, t.description, t.default_ttl, t.created_by, t.icon, t.user_acl, t.group_acl, t.display_name, t.allow_user_cancel_workspace_jobs, t.allow_user_autostart, t.allow_user_autostop, t.failure_ttl, t.time_til_dormant, t.time_til_dormant_autodelete, t.autostop_requirement_days_of_week, t.autostop_requirement_weeks, t.autostart_block_days_of_week, t.require_active_version, t.deprecated, t.activity_bump, t.max_port_sharing_level, t.use_classic_parameter_flow, t.cors_behavior, t.disable_module_cache, t.time_til_autostop_notify, t.created_by_avatar_url, t.created_by_username, t.created_by_name, t.organization_name, t.organization_display_name, t.organization_icon
|
||||
t.id, t.created_at, t.updated_at, t.organization_id, t.deleted, t.name, t.provisioner, t.active_version_id, t.description, t.default_ttl, t.created_by, t.icon, t.user_acl, t.group_acl, t.display_name, t.allow_user_cancel_workspace_jobs, t.allow_user_autostart, t.allow_user_autostop, t.failure_ttl, t.time_til_dormant, t.time_til_dormant_autodelete, t.autostop_requirement_days_of_week, t.autostop_requirement_weeks, t.autostart_block_days_of_week, t.require_active_version, t.deprecated, t.activity_bump, t.max_port_sharing_level, t.use_classic_parameter_flow, t.cors_behavior, t.disable_module_cache, t.time_til_autostop_notify, t.agents_allowed, t.created_by_avatar_url, t.created_by_username, t.created_by_name, t.organization_name, t.organization_display_name, t.organization_icon
|
||||
FROM
|
||||
template_with_names AS t
|
||||
LEFT JOIN
|
||||
@@ -26974,23 +26977,29 @@ WHERE
|
||||
tv.has_ai_task = $9 :: boolean
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by agents_allowed
|
||||
AND CASE
|
||||
WHEN $10 :: boolean IS NOT NULL THEN
|
||||
t.agents_allowed = $10 :: boolean
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by author_id
|
||||
AND CASE
|
||||
WHEN $10 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
|
||||
t.created_by = $10
|
||||
WHEN $11 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
|
||||
t.created_by = $11
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by author_username
|
||||
AND CASE
|
||||
WHEN $11 :: text != '' THEN
|
||||
t.created_by = (SELECT id FROM users WHERE lower(users.username) = lower($11) AND deleted = false)
|
||||
WHEN $12 :: text != '' THEN
|
||||
t.created_by = (SELECT id FROM users WHERE lower(users.username) = lower($12) AND deleted = false)
|
||||
ELSE true
|
||||
END
|
||||
|
||||
-- Filter by has_external_agent in latest version
|
||||
AND CASE
|
||||
WHEN $12 :: boolean IS NOT NULL THEN
|
||||
tv.has_external_agent = $12 :: boolean
|
||||
WHEN $13 :: boolean IS NOT NULL THEN
|
||||
tv.has_external_agent = $13 :: boolean
|
||||
ELSE true
|
||||
END
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedTemplates
|
||||
@@ -27008,6 +27017,7 @@ type GetTemplatesWithFilterParams struct {
|
||||
IDs []uuid.UUID `db:"ids" json:"ids"`
|
||||
Deprecated sql.NullBool `db:"deprecated" json:"deprecated"`
|
||||
HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"`
|
||||
AgentsAllowed sql.NullBool `db:"agents_allowed" json:"agents_allowed"`
|
||||
AuthorID uuid.UUID `db:"author_id" json:"author_id"`
|
||||
AuthorUsername string `db:"author_username" json:"author_username"`
|
||||
HasExternalAgent sql.NullBool `db:"has_external_agent" json:"has_external_agent"`
|
||||
@@ -27024,6 +27034,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
|
||||
pq.Array(arg.IDs),
|
||||
arg.Deprecated,
|
||||
arg.HasAITask,
|
||||
arg.AgentsAllowed,
|
||||
arg.AuthorID,
|
||||
arg.AuthorUsername,
|
||||
arg.HasExternalAgent,
|
||||
@@ -27068,6 +27079,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
|
||||
&i.CorsBehavior,
|
||||
&i.DisableModuleCache,
|
||||
&i.TimeTilAutostopNotify,
|
||||
&i.AgentsAllowed,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
&i.CreatedByName,
|
||||
@@ -27107,10 +27119,11 @@ INSERT INTO
|
||||
allow_user_cancel_workspace_jobs,
|
||||
max_port_sharing_level,
|
||||
use_classic_parameter_flow,
|
||||
cors_behavior
|
||||
cors_behavior,
|
||||
agents_allowed
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
||||
`
|
||||
|
||||
type InsertTemplateParams struct {
|
||||
@@ -27131,6 +27144,7 @@ type InsertTemplateParams struct {
|
||||
MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
|
||||
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
|
||||
CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"`
|
||||
AgentsAllowed bool `db:"agents_allowed" json:"agents_allowed"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParams) error {
|
||||
@@ -27152,6 +27166,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
|
||||
arg.MaxPortSharingLevel,
|
||||
arg.UseClassicParameterFlow,
|
||||
arg.CorsBehavior,
|
||||
arg.AgentsAllowed,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -27254,7 +27269,8 @@ SET
|
||||
max_port_sharing_level = $9,
|
||||
use_classic_parameter_flow = $10,
|
||||
cors_behavior = $11,
|
||||
disable_module_cache = $12
|
||||
disable_module_cache = $12,
|
||||
agents_allowed = $13
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
@@ -27272,6 +27288,7 @@ type UpdateTemplateMetaByIDParams struct {
|
||||
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
|
||||
CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"`
|
||||
DisableModuleCache bool `db:"disable_module_cache" json:"disable_module_cache"`
|
||||
AgentsAllowed bool `db:"agents_allowed" json:"agents_allowed"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error {
|
||||
@@ -27288,6 +27305,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
|
||||
arg.UseClassicParameterFlow,
|
||||
arg.CorsBehavior,
|
||||
arg.DisableModuleCache,
|
||||
arg.AgentsAllowed,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -38570,7 +38588,7 @@ LEFT JOIN LATERAL (
|
||||
) latest_build ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, agents_allowed
|
||||
FROM
|
||||
templates
|
||||
WHERE
|
||||
|
||||
@@ -77,6 +77,12 @@ WHERE
|
||||
tv.has_ai_task = sqlc.narg('has_ai_task') :: boolean
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by agents_allowed
|
||||
AND CASE
|
||||
WHEN sqlc.narg('agents_allowed') :: boolean IS NOT NULL THEN
|
||||
t.agents_allowed = sqlc.narg('agents_allowed') :: boolean
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by author_id
|
||||
AND CASE
|
||||
WHEN @author_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
|
||||
@@ -137,10 +143,11 @@ INSERT INTO
|
||||
allow_user_cancel_workspace_jobs,
|
||||
max_port_sharing_level,
|
||||
use_classic_parameter_flow,
|
||||
cors_behavior
|
||||
cors_behavior,
|
||||
agents_allowed
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17);
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18);
|
||||
|
||||
-- name: UpdateTemplateActiveVersionByID :exec
|
||||
UPDATE
|
||||
@@ -174,7 +181,8 @@ SET
|
||||
max_port_sharing_level = $9,
|
||||
use_classic_parameter_flow = $10,
|
||||
cors_behavior = $11,
|
||||
disable_module_cache = $12
|
||||
disable_module_cache = $12,
|
||||
agents_allowed = $13
|
||||
WHERE
|
||||
id = $1
|
||||
;
|
||||
|
||||
@@ -555,6 +555,7 @@ func (api *API) templateBuilderCreateTemplate(rw http.ResponseWriter, r *http.Re
|
||||
MaxPortSharingLevel: database.AppSharingLevelOwner,
|
||||
UseClassicParameterFlow: false,
|
||||
CorsBehavior: database.CorsBehaviorSimple,
|
||||
AgentsAllowed: true,
|
||||
})
|
||||
if err != nil {
|
||||
if database.IsUniqueViolation(err, database.UniqueTemplatesOrganizationIDNameIndex) {
|
||||
|
||||
@@ -224,6 +224,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
||||
Icon: createTemplate.Icon,
|
||||
DisplayName: createTemplate.DisplayName,
|
||||
UseClassicParameterFlow: useClassicParameterFlow,
|
||||
AgentsAllowed: true,
|
||||
}
|
||||
|
||||
_, err := api.Database.GetTemplateByOrganizationAndName(ctx, database.GetTemplateByOrganizationAndNameParams{
|
||||
@@ -447,6 +448,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
||||
MaxPortSharingLevel: maxPortShareLevel,
|
||||
UseClassicParameterFlow: useClassicParameterFlow,
|
||||
CorsBehavior: corsBehavior,
|
||||
AgentsAllowed: true,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert template: %s", err)
|
||||
@@ -778,6 +780,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
UseClassicParameterFlow: resolved.useClassicTemplateFlow,
|
||||
CorsBehavior: resolved.corsBehavior,
|
||||
DisableModuleCache: resolved.disableModuleCache,
|
||||
AgentsAllowed: template.AgentsAllowed,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("update template metadata: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user