Skip to content

Commit 599bb35

Browse files
authored
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
1 parent 7f056da commit 599bb35

File tree

6 files changed

+329
-1
lines changed

6 files changed

+329
-1
lines changed

coderd/apidoc/docs.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmem/dbmem.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1380,6 +1380,12 @@ func (q *FakeQuerier) getProvisionerJobsByIDsWithQueuePositionLockedGlobalQueue(
13801380
return jobs, nil
13811381
}
13821382

1383+
// isDeprecated returns true if the template is deprecated.
1384+
// A template is considered deprecated when it has a deprecation message.
1385+
func isDeprecated(template database.Template) bool {
1386+
return template.Deprecated != ""
1387+
}
1388+
13831389
func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error {
13841390
return xerrors.New("AcquireLock must only be called within a transaction")
13851391
}
@@ -13023,7 +13029,17 @@ func (q *FakeQuerier) GetAuthorizedTemplates(ctx context.Context, arg database.G
1302313029
if arg.ExactName != "" && !strings.EqualFold(template.Name, arg.ExactName) {
1302413030
continue
1302513031
}
13026-
if arg.Deprecated.Valid && arg.Deprecated.Bool == (template.Deprecated != "") {
13032+
// Filters templates based on the search query filter 'Deprecated' status
13033+
// Matching SQL logic:
13034+
// -- Filter by deprecated
13035+
// AND CASE
13036+
// WHEN :deprecated IS NOT NULL THEN
13037+
// CASE
13038+
// WHEN :deprecated THEN deprecated != ''
13039+
// ELSE deprecated = ''
13040+
// END
13041+
// ELSE true
13042+
if arg.Deprecated.Valid && arg.Deprecated.Bool != isDeprecated(template) {
1302713043
continue
1302813044
}
1302913045
if arg.FuzzyName != "" {

coderd/templates.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,9 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
487487
}
488488

489489
// @Summary Get templates by organization
490+
// @Description Returns a list of templates for the specified organization.
491+
// @Description By default, only non-deprecated templates are returned.
492+
// @Description To include deprecated templates, specify `deprecated:true` in the search query.
490493
// @ID get-templates-by-organization
491494
// @Security CoderSessionToken
492495
// @Produce json
@@ -506,6 +509,9 @@ func (api *API) templatesByOrganization() http.HandlerFunc {
506509
}
507510

508511
// @Summary Get all templates
512+
// @Description Returns a list of templates.
513+
// @Description By default, only non-deprecated templates are returned.
514+
// @Description To include deprecated templates, specify `deprecated:true` in the search query.
509515
// @ID get-all-templates
510516
// @Security CoderSessionToken
511517
// @Produce json
@@ -540,6 +546,14 @@ func (api *API) fetchTemplates(mutate func(r *http.Request, arg *database.GetTem
540546
mutate(r, &args)
541547
}
542548

549+
// By default, deprecated templates are excluded unless explicitly requested
550+
if !args.Deprecated.Valid {
551+
args.Deprecated = sql.NullBool{
552+
Bool: false,
553+
Valid: true,
554+
}
555+
}
556+
543557
// Filter templates based on rbac permissions
544558
templates, err := api.Database.GetAuthorizedTemplates(ctx, args, prepared)
545559
if errors.Is(err, sql.ErrNoRows) {

coderd/templates_test.go

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,250 @@ func TestPostTemplateByOrganization(t *testing.T) {
441441
})
442442
}
443443

444+
func TestTemplates(t *testing.T) {
445+
t.Parallel()
446+
447+
t.Run("ListEmpty", func(t *testing.T) {
448+
t.Parallel()
449+
client := coderdtest.New(t, nil)
450+
_ = coderdtest.CreateFirstUser(t, client)
451+
452+
ctx := testutil.Context(t, testutil.WaitLong)
453+
454+
templates, err := client.Templates(ctx, codersdk.TemplateFilter{})
455+
require.NoError(t, err)
456+
require.NotNil(t, templates)
457+
require.Len(t, templates, 0)
458+
})
459+
460+
// Should return only non-deprecated templates by default
461+
t.Run("ListMultiple non-deprecated", func(t *testing.T) {
462+
t.Parallel()
463+
464+
owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: false})
465+
user := coderdtest.CreateFirstUser(t, owner)
466+
client, tplAdmin := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
467+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
468+
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
469+
foo := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
470+
request.Name = "foo"
471+
})
472+
bar := coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID, func(request *codersdk.CreateTemplateRequest) {
473+
request.Name = "bar"
474+
})
475+
476+
ctx := testutil.Context(t, testutil.WaitLong)
477+
478+
// Deprecate bar template
479+
deprecationMessage := "Some deprecated message"
480+
err := db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin, user.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{
481+
ID: bar.ID,
482+
RequireActiveVersion: false,
483+
Deprecated: deprecationMessage,
484+
})
485+
require.NoError(t, err)
486+
487+
updatedBar, err := client.Template(ctx, bar.ID)
488+
require.NoError(t, err)
489+
require.True(t, updatedBar.Deprecated)
490+
require.Equal(t, deprecationMessage, updatedBar.DeprecationMessage)
491+
492+
// Should return only the non-deprecated template (foo)
493+
templates, err := client.Templates(ctx, codersdk.TemplateFilter{})
494+
require.NoError(t, err)
495+
require.Len(t, templates, 1)
496+
497+
require.Equal(t, foo.ID, templates[0].ID)
498+
require.False(t, templates[0].Deprecated)
499+
require.Empty(t, templates[0].DeprecationMessage)
500+
})
501+
502+
// Should return only deprecated templates when filtering by deprecated:true
503+
t.Run("ListMultiple deprecated:true", func(t *testing.T) {
504+
t.Parallel()
505+
506+
owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: false})
507+
user := coderdtest.CreateFirstUser(t, owner)
508+
client, tplAdmin := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
509+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
510+
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
511+
foo := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
512+
request.Name = "foo"
513+
})
514+
bar := coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID, func(request *codersdk.CreateTemplateRequest) {
515+
request.Name = "bar"
516+
})
517+
518+
ctx := testutil.Context(t, testutil.WaitLong)
519+
520+
// Deprecate foo and bar templates
521+
deprecationMessage := "Some deprecated message"
522+
err := db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin, user.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{
523+
ID: foo.ID,
524+
RequireActiveVersion: false,
525+
Deprecated: deprecationMessage,
526+
})
527+
require.NoError(t, err)
528+
err = db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin, user.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{
529+
ID: bar.ID,
530+
RequireActiveVersion: false,
531+
Deprecated: deprecationMessage,
532+
})
533+
require.NoError(t, err)
534+
535+
// Should have deprecation message set
536+
updatedFoo, err := client.Template(ctx, foo.ID)
537+
require.NoError(t, err)
538+
require.True(t, updatedFoo.Deprecated)
539+
require.Equal(t, deprecationMessage, updatedFoo.DeprecationMessage)
540+
541+
updatedBar, err := client.Template(ctx, bar.ID)
542+
require.NoError(t, err)
543+
require.True(t, updatedBar.Deprecated)
544+
require.Equal(t, deprecationMessage, updatedBar.DeprecationMessage)
545+
546+
// Should return only the deprecated templates (foo and bar)
547+
templates, err := client.Templates(ctx, codersdk.TemplateFilter{
548+
SearchQuery: "deprecated:true",
549+
})
550+
require.NoError(t, err)
551+
require.Len(t, templates, 2)
552+
553+
// Make sure all the deprecated templates are returned
554+
expectedTemplates := map[uuid.UUID]codersdk.Template{
555+
updatedFoo.ID: updatedFoo,
556+
updatedBar.ID: updatedBar,
557+
}
558+
actualTemplates := map[uuid.UUID]codersdk.Template{}
559+
for _, template := range templates {
560+
actualTemplates[template.ID] = template
561+
}
562+
563+
require.Equal(t, len(expectedTemplates), len(actualTemplates))
564+
for id, expectedTemplate := range expectedTemplates {
565+
actualTemplate, ok := actualTemplates[id]
566+
require.True(t, ok)
567+
require.Equal(t, expectedTemplate.ID, actualTemplate.ID)
568+
require.Equal(t, true, actualTemplate.Deprecated)
569+
require.Equal(t, expectedTemplate.DeprecationMessage, actualTemplate.DeprecationMessage)
570+
}
571+
})
572+
573+
// Should return only non-deprecated templates when filtering by deprecated:false
574+
t.Run("ListMultiple deprecated:false", func(t *testing.T) {
575+
t.Parallel()
576+
577+
client := coderdtest.New(t, nil)
578+
user := coderdtest.CreateFirstUser(t, client)
579+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
580+
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
581+
foo := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
582+
request.Name = "foo"
583+
})
584+
bar := coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID, func(request *codersdk.CreateTemplateRequest) {
585+
request.Name = "bar"
586+
})
587+
588+
ctx := testutil.Context(t, testutil.WaitLong)
589+
590+
// Should return only the non-deprecated templates
591+
templates, err := client.Templates(ctx, codersdk.TemplateFilter{
592+
SearchQuery: "deprecated:false",
593+
})
594+
require.NoError(t, err)
595+
require.Len(t, templates, 2)
596+
597+
// Make sure all the non-deprecated templates are returned
598+
expectedTemplates := map[uuid.UUID]codersdk.Template{
599+
foo.ID: foo,
600+
bar.ID: bar,
601+
}
602+
actualTemplates := map[uuid.UUID]codersdk.Template{}
603+
for _, template := range templates {
604+
actualTemplates[template.ID] = template
605+
}
606+
607+
require.Equal(t, len(expectedTemplates), len(actualTemplates))
608+
for id, expectedTemplate := range expectedTemplates {
609+
actualTemplate, ok := actualTemplates[id]
610+
require.True(t, ok)
611+
require.Equal(t, expectedTemplate.ID, actualTemplate.ID)
612+
require.Equal(t, false, actualTemplate.Deprecated)
613+
require.Equal(t, expectedTemplate.DeprecationMessage, actualTemplate.DeprecationMessage)
614+
}
615+
})
616+
617+
// Should return a re-enabled template in the default (non-deprecated) list
618+
t.Run("ListMultiple re-enabled template", func(t *testing.T) {
619+
t.Parallel()
620+
621+
owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: false})
622+
user := coderdtest.CreateFirstUser(t, owner)
623+
client, tplAdmin := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
624+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
625+
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
626+
foo := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
627+
request.Name = "foo"
628+
})
629+
bar := coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID, func(request *codersdk.CreateTemplateRequest) {
630+
request.Name = "bar"
631+
})
632+
633+
ctx := testutil.Context(t, testutil.WaitLong)
634+
635+
// Deprecate bar template
636+
deprecationMessage := "Some deprecated message"
637+
err := db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin, user.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{
638+
ID: bar.ID,
639+
RequireActiveVersion: false,
640+
Deprecated: deprecationMessage,
641+
})
642+
require.NoError(t, err)
643+
644+
updatedBar, err := client.Template(ctx, bar.ID)
645+
require.NoError(t, err)
646+
require.True(t, updatedBar.Deprecated)
647+
require.Equal(t, deprecationMessage, updatedBar.DeprecationMessage)
648+
649+
// Re-enable bar template
650+
err = db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin, user.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{
651+
ID: bar.ID,
652+
RequireActiveVersion: false,
653+
Deprecated: "",
654+
})
655+
require.NoError(t, err)
656+
657+
reEnabledBar, err := client.Template(ctx, bar.ID)
658+
require.NoError(t, err)
659+
require.False(t, reEnabledBar.Deprecated)
660+
require.Empty(t, reEnabledBar.DeprecationMessage)
661+
662+
// Should return only the non-deprecated templates (foo and bar)
663+
templates, err := client.Templates(ctx, codersdk.TemplateFilter{})
664+
require.NoError(t, err)
665+
require.Len(t, templates, 2)
666+
667+
// Make sure all the non-deprecated templates are returned
668+
expectedTemplates := map[uuid.UUID]codersdk.Template{
669+
foo.ID: foo,
670+
bar.ID: bar,
671+
}
672+
actualTemplates := map[uuid.UUID]codersdk.Template{}
673+
for _, template := range templates {
674+
actualTemplates[template.ID] = template
675+
}
676+
677+
require.Equal(t, len(expectedTemplates), len(actualTemplates))
678+
for id, expectedTemplate := range expectedTemplates {
679+
actualTemplate, ok := actualTemplates[id]
680+
require.True(t, ok)
681+
require.Equal(t, expectedTemplate.ID, actualTemplate.ID)
682+
require.Equal(t, false, actualTemplate.Deprecated)
683+
require.Equal(t, expectedTemplate.DeprecationMessage, actualTemplate.DeprecationMessage)
684+
}
685+
})
686+
}
687+
444688
func TestTemplatesByOrganization(t *testing.T) {
445689
t.Parallel()
446690
t.Run("ListEmpty", func(t *testing.T) {
@@ -525,6 +769,48 @@ func TestTemplatesByOrganization(t *testing.T) {
525769
require.Len(t, templates, 1)
526770
require.Equal(t, bar.ID, templates[0].ID)
527771
})
772+
773+
// Should return only non-deprecated templates by default
774+
t.Run("ListMultiple non-deprecated", func(t *testing.T) {
775+
t.Parallel()
776+
777+
owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: false})
778+
user := coderdtest.CreateFirstUser(t, owner)
779+
client, tplAdmin := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
780+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
781+
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
782+
foo := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
783+
request.Name = "foo"
784+
})
785+
bar := coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID, func(request *codersdk.CreateTemplateRequest) {
786+
request.Name = "bar"
787+
})
788+
789+
ctx := testutil.Context(t, testutil.WaitLong)
790+
791+
// Deprecate bar template
792+
deprecationMessage := "Some deprecated message"
793+
err := db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin, user.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{
794+
ID: bar.ID,
795+
RequireActiveVersion: false,
796+
Deprecated: deprecationMessage,
797+
})
798+
require.NoError(t, err)
799+
800+
updatedBar, err := client.Template(ctx, bar.ID)
801+
require.NoError(t, err)
802+
require.True(t, updatedBar.Deprecated)
803+
require.Equal(t, deprecationMessage, updatedBar.DeprecationMessage)
804+
805+
// Should return only the non-deprecated template (foo)
806+
templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID)
807+
require.NoError(t, err)
808+
require.Len(t, templates, 1)
809+
810+
require.Equal(t, foo.ID, templates[0].ID)
811+
require.False(t, templates[0].Deprecated)
812+
require.Empty(t, templates[0].DeprecationMessage)
813+
})
528814
}
529815

530816
func TestTemplateByOrganizationAndName(t *testing.T) {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy