Skip to content

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

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 8 commits into from
May 13, 2025
Merged
2 changes: 2 additions & 0 deletions coderd/apidoc/docs.go

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

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

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

18 changes: 17 additions & 1 deletion coderd/database/dbmem/dbmem.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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 != "" {
Expand Down
14 changes: 14 additions & 0 deletions coderd/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
286 changes: 286 additions & 0 deletions coderd/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
Loading
Loading