mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
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:
2
coderd/apidoc/docs.go
generated
2
coderd/apidoc/docs.go
generated
@ -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"
|
||||
],
|
||||
|
2
coderd/apidoc/swagger.json
generated
2
coderd/apidoc/swagger.json
generated
@ -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",
|
||||
|
@ -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 != "" {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
Reference in New Issue
Block a user