fix(coderd): list templates returns non-deprecated templates by default (#17747)

## Description

Modifies the behaviour of the "list templates" API endpoints to return
non-deprecated templates by default. Users can still query for
deprecated templates by specifying the `deprecated=true` query
parameter.

**Note:** The deprecation feature is an enterprise-level feature

## Affected Endpoints
* /api/v2/organizations/{organization}/templates
* /api/v2/templates

Fixes #17565
This commit is contained in:
Susana Ferreira
2025-05-13 12:44:46 +01:00
committed by GitHub
parent 7f056da088
commit 599bb35a04
6 changed files with 329 additions and 1 deletions

2
coderd/apidoc/docs.go generated
View File

@ -4109,6 +4109,7 @@ const docTemplate = `{
"CoderSessionToken": []
}
],
"description": "Returns a list of templates for the specified organization.\nBy default, only non-deprecated templates are returned.\nTo include deprecated templates, specify ` + "`" + `deprecated:true` + "`" + ` in the search query.",
"produces": [
"application/json"
],
@ -4936,6 +4937,7 @@ const docTemplate = `{
"CoderSessionToken": []
}
],
"description": "Returns a list of templates.\nBy default, only non-deprecated templates are returned.\nTo include deprecated templates, specify ` + "`" + `deprecated:true` + "`" + ` in the search query.",
"produces": [
"application/json"
],

View File

@ -3628,6 +3628,7 @@
"CoderSessionToken": []
}
],
"description": "Returns a list of templates for the specified organization.\nBy default, only non-deprecated templates are returned.\nTo include deprecated templates, specify `deprecated:true` in the search query.",
"produces": ["application/json"],
"tags": ["Templates"],
"summary": "Get templates by organization",
@ -4355,6 +4356,7 @@
"CoderSessionToken": []
}
],
"description": "Returns a list of templates.\nBy default, only non-deprecated templates are returned.\nTo include deprecated templates, specify `deprecated:true` in the search query.",
"produces": ["application/json"],
"tags": ["Templates"],
"summary": "Get all templates",

View File

@ -1380,6 +1380,12 @@ func (q *FakeQuerier) getProvisionerJobsByIDsWithQueuePositionLockedGlobalQueue(
return jobs, nil
}
// isDeprecated returns true if the template is deprecated.
// A template is considered deprecated when it has a deprecation message.
func isDeprecated(template database.Template) bool {
return template.Deprecated != ""
}
func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error {
return xerrors.New("AcquireLock must only be called within a transaction")
}
@ -13023,7 +13029,17 @@ func (q *FakeQuerier) GetAuthorizedTemplates(ctx context.Context, arg database.G
if arg.ExactName != "" && !strings.EqualFold(template.Name, arg.ExactName) {
continue
}
if arg.Deprecated.Valid && arg.Deprecated.Bool == (template.Deprecated != "") {
// Filters templates based on the search query filter 'Deprecated' status
// Matching SQL logic:
// -- Filter by deprecated
// AND CASE
// WHEN :deprecated IS NOT NULL THEN
// CASE
// WHEN :deprecated THEN deprecated != ''
// ELSE deprecated = ''
// END
// ELSE true
if arg.Deprecated.Valid && arg.Deprecated.Bool != isDeprecated(template) {
continue
}
if arg.FuzzyName != "" {

View File

@ -487,6 +487,9 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
}
// @Summary Get templates by organization
// @Description Returns a list of templates for the specified organization.
// @Description By default, only non-deprecated templates are returned.
// @Description To include deprecated templates, specify `deprecated:true` in the search query.
// @ID get-templates-by-organization
// @Security CoderSessionToken
// @Produce json
@ -506,6 +509,9 @@ func (api *API) templatesByOrganization() http.HandlerFunc {
}
// @Summary Get all templates
// @Description Returns a list of templates.
// @Description By default, only non-deprecated templates are returned.
// @Description To include deprecated templates, specify `deprecated:true` in the search query.
// @ID get-all-templates
// @Security CoderSessionToken
// @Produce json
@ -540,6 +546,14 @@ func (api *API) fetchTemplates(mutate func(r *http.Request, arg *database.GetTem
mutate(r, &args)
}
// By default, deprecated templates are excluded unless explicitly requested
if !args.Deprecated.Valid {
args.Deprecated = sql.NullBool{
Bool: false,
Valid: true,
}
}
// Filter templates based on rbac permissions
templates, err := api.Database.GetAuthorizedTemplates(ctx, args, prepared)
if errors.Is(err, sql.ErrNoRows) {

View File

@ -441,6 +441,250 @@ func TestPostTemplateByOrganization(t *testing.T) {
})
}
func TestTemplates(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitLong)
templates, err := client.Templates(ctx, codersdk.TemplateFilter{})
require.NoError(t, err)
require.NotNil(t, templates)
require.Len(t, templates, 0)
})
// Should return only non-deprecated templates by default
t.Run("ListMultiple non-deprecated", func(t *testing.T) {
t.Parallel()
owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: false})
user := coderdtest.CreateFirstUser(t, owner)
client, tplAdmin := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
foo := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
request.Name = "foo"
})
bar := coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID, func(request *codersdk.CreateTemplateRequest) {
request.Name = "bar"
})
ctx := testutil.Context(t, testutil.WaitLong)
// Deprecate bar template
deprecationMessage := "Some deprecated message"
err := db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin, user.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{
ID: bar.ID,
RequireActiveVersion: false,
Deprecated: deprecationMessage,
})
require.NoError(t, err)
updatedBar, err := client.Template(ctx, bar.ID)
require.NoError(t, err)
require.True(t, updatedBar.Deprecated)
require.Equal(t, deprecationMessage, updatedBar.DeprecationMessage)
// Should return only the non-deprecated template (foo)
templates, err := client.Templates(ctx, codersdk.TemplateFilter{})
require.NoError(t, err)
require.Len(t, templates, 1)
require.Equal(t, foo.ID, templates[0].ID)
require.False(t, templates[0].Deprecated)
require.Empty(t, templates[0].DeprecationMessage)
})
// Should return only deprecated templates when filtering by deprecated:true
t.Run("ListMultiple deprecated:true", func(t *testing.T) {
t.Parallel()
owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: false})
user := coderdtest.CreateFirstUser(t, owner)
client, tplAdmin := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
foo := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
request.Name = "foo"
})
bar := coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID, func(request *codersdk.CreateTemplateRequest) {
request.Name = "bar"
})
ctx := testutil.Context(t, testutil.WaitLong)
// Deprecate foo and bar templates
deprecationMessage := "Some deprecated message"
err := db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin, user.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{
ID: foo.ID,
RequireActiveVersion: false,
Deprecated: deprecationMessage,
})
require.NoError(t, err)
err = db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin, user.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{
ID: bar.ID,
RequireActiveVersion: false,
Deprecated: deprecationMessage,
})
require.NoError(t, err)
// Should have deprecation message set
updatedFoo, err := client.Template(ctx, foo.ID)
require.NoError(t, err)
require.True(t, updatedFoo.Deprecated)
require.Equal(t, deprecationMessage, updatedFoo.DeprecationMessage)
updatedBar, err := client.Template(ctx, bar.ID)
require.NoError(t, err)
require.True(t, updatedBar.Deprecated)
require.Equal(t, deprecationMessage, updatedBar.DeprecationMessage)
// Should return only the deprecated templates (foo and bar)
templates, err := client.Templates(ctx, codersdk.TemplateFilter{
SearchQuery: "deprecated:true",
})
require.NoError(t, err)
require.Len(t, templates, 2)
// Make sure all the deprecated templates are returned
expectedTemplates := map[uuid.UUID]codersdk.Template{
updatedFoo.ID: updatedFoo,
updatedBar.ID: updatedBar,
}
actualTemplates := map[uuid.UUID]codersdk.Template{}
for _, template := range templates {
actualTemplates[template.ID] = template
}
require.Equal(t, len(expectedTemplates), len(actualTemplates))
for id, expectedTemplate := range expectedTemplates {
actualTemplate, ok := actualTemplates[id]
require.True(t, ok)
require.Equal(t, expectedTemplate.ID, actualTemplate.ID)
require.Equal(t, true, actualTemplate.Deprecated)
require.Equal(t, expectedTemplate.DeprecationMessage, actualTemplate.DeprecationMessage)
}
})
// Should return only non-deprecated templates when filtering by deprecated:false
t.Run("ListMultiple deprecated:false", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
foo := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
request.Name = "foo"
})
bar := coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID, func(request *codersdk.CreateTemplateRequest) {
request.Name = "bar"
})
ctx := testutil.Context(t, testutil.WaitLong)
// Should return only the non-deprecated templates
templates, err := client.Templates(ctx, codersdk.TemplateFilter{
SearchQuery: "deprecated:false",
})
require.NoError(t, err)
require.Len(t, templates, 2)
// Make sure all the non-deprecated templates are returned
expectedTemplates := map[uuid.UUID]codersdk.Template{
foo.ID: foo,
bar.ID: bar,
}
actualTemplates := map[uuid.UUID]codersdk.Template{}
for _, template := range templates {
actualTemplates[template.ID] = template
}
require.Equal(t, len(expectedTemplates), len(actualTemplates))
for id, expectedTemplate := range expectedTemplates {
actualTemplate, ok := actualTemplates[id]
require.True(t, ok)
require.Equal(t, expectedTemplate.ID, actualTemplate.ID)
require.Equal(t, false, actualTemplate.Deprecated)
require.Equal(t, expectedTemplate.DeprecationMessage, actualTemplate.DeprecationMessage)
}
})
// Should return a re-enabled template in the default (non-deprecated) list
t.Run("ListMultiple re-enabled template", func(t *testing.T) {
t.Parallel()
owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: false})
user := coderdtest.CreateFirstUser(t, owner)
client, tplAdmin := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
foo := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
request.Name = "foo"
})
bar := coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID, func(request *codersdk.CreateTemplateRequest) {
request.Name = "bar"
})
ctx := testutil.Context(t, testutil.WaitLong)
// Deprecate bar template
deprecationMessage := "Some deprecated message"
err := db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin, user.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{
ID: bar.ID,
RequireActiveVersion: false,
Deprecated: deprecationMessage,
})
require.NoError(t, err)
updatedBar, err := client.Template(ctx, bar.ID)
require.NoError(t, err)
require.True(t, updatedBar.Deprecated)
require.Equal(t, deprecationMessage, updatedBar.DeprecationMessage)
// Re-enable bar template
err = db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin, user.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{
ID: bar.ID,
RequireActiveVersion: false,
Deprecated: "",
})
require.NoError(t, err)
reEnabledBar, err := client.Template(ctx, bar.ID)
require.NoError(t, err)
require.False(t, reEnabledBar.Deprecated)
require.Empty(t, reEnabledBar.DeprecationMessage)
// Should return only the non-deprecated templates (foo and bar)
templates, err := client.Templates(ctx, codersdk.TemplateFilter{})
require.NoError(t, err)
require.Len(t, templates, 2)
// Make sure all the non-deprecated templates are returned
expectedTemplates := map[uuid.UUID]codersdk.Template{
foo.ID: foo,
bar.ID: bar,
}
actualTemplates := map[uuid.UUID]codersdk.Template{}
for _, template := range templates {
actualTemplates[template.ID] = template
}
require.Equal(t, len(expectedTemplates), len(actualTemplates))
for id, expectedTemplate := range expectedTemplates {
actualTemplate, ok := actualTemplates[id]
require.True(t, ok)
require.Equal(t, expectedTemplate.ID, actualTemplate.ID)
require.Equal(t, false, actualTemplate.Deprecated)
require.Equal(t, expectedTemplate.DeprecationMessage, actualTemplate.DeprecationMessage)
}
})
}
func TestTemplatesByOrganization(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
@ -525,6 +769,48 @@ func TestTemplatesByOrganization(t *testing.T) {
require.Len(t, templates, 1)
require.Equal(t, bar.ID, templates[0].ID)
})
// Should return only non-deprecated templates by default
t.Run("ListMultiple non-deprecated", func(t *testing.T) {
t.Parallel()
owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: false})
user := coderdtest.CreateFirstUser(t, owner)
client, tplAdmin := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
foo := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
request.Name = "foo"
})
bar := coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID, func(request *codersdk.CreateTemplateRequest) {
request.Name = "bar"
})
ctx := testutil.Context(t, testutil.WaitLong)
// Deprecate bar template
deprecationMessage := "Some deprecated message"
err := db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin, user.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{
ID: bar.ID,
RequireActiveVersion: false,
Deprecated: deprecationMessage,
})
require.NoError(t, err)
updatedBar, err := client.Template(ctx, bar.ID)
require.NoError(t, err)
require.True(t, updatedBar.Deprecated)
require.Equal(t, deprecationMessage, updatedBar.DeprecationMessage)
// Should return only the non-deprecated template (foo)
templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID)
require.NoError(t, err)
require.Len(t, templates, 1)
require.Equal(t, foo.ID, templates[0].ID)
require.False(t, templates[0].Deprecated)
require.Empty(t, templates[0].DeprecationMessage)
})
}
func TestTemplateByOrganizationAndName(t *testing.T) {