chore: implement fetch all authorized templates api (#13678)

This commit is contained in:
Steven Masley
2024-06-26 11:50:32 -06:00
committed by GitHub
parent 08e728bcb2
commit 30c4b4db5c
7 changed files with 315 additions and 54 deletions
+28
View File
@@ -3101,6 +3101,34 @@ const docTemplate = `{
}
}
},
"/templates": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"Templates"
],
"summary": "Get all templates",
"operationId": "get-all-templates",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.Template"
}
}
}
}
}
},
"/templates/{template}": {
"get": {
"security": [
+24
View File
@@ -2725,6 +2725,30 @@
}
}
},
"/templates": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"tags": ["Templates"],
"summary": "Get all templates",
"operationId": "get-all-templates",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.Template"
}
}
}
}
}
},
"/templates/{template}": {
"get": {
"security": [
+17 -12
View File
@@ -827,7 +827,7 @@ func New(options *Options) *API {
r.Post("/templateversions", api.postTemplateVersionsByOrganization)
r.Route("/templates", func(r chi.Router) {
r.Post("/", api.postTemplateByOrganization)
r.Get("/", api.templatesByOrganization)
r.Get("/", api.templatesByOrganization())
r.Get("/examples", api.templateExamples)
r.Route("/{templatename}", func(r chi.Router) {
r.Get("/", api.templateByOrganizationAndName)
@@ -869,20 +869,25 @@ func New(options *Options) *API {
})
})
})
r.Route("/templates/{template}", func(r chi.Router) {
r.Route("/templates", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
httpmw.ExtractTemplateParam(options.Database),
)
r.Get("/daus", api.templateDAUs)
r.Get("/", api.template)
r.Delete("/", api.deleteTemplate)
r.Patch("/", api.patchTemplateMeta)
r.Route("/versions", func(r chi.Router) {
r.Post("/archive", api.postArchiveTemplateVersions)
r.Get("/", api.templateVersionsByTemplate)
r.Patch("/", api.patchActiveTemplateVersion)
r.Get("/{templateversionname}", api.templateVersionByName)
r.Get("/", api.fetchTemplates(nil))
r.Route("/{template}", func(r chi.Router) {
r.Use(
httpmw.ExtractTemplateParam(options.Database),
)
r.Get("/daus", api.templateDAUs)
r.Get("/", api.template)
r.Delete("/", api.deleteTemplate)
r.Patch("/", api.patchTemplateMeta)
r.Route("/versions", func(r chi.Router) {
r.Post("/archive", api.postArchiveTemplateVersions)
r.Get("/", api.templateVersionsByTemplate)
r.Patch("/", api.patchActiveTemplateVersion)
r.Get("/{templateversionname}", api.templateVersionByName)
})
})
})
r.Route("/templateversions/{templateversion}", func(r chi.Router) {
+65 -42
View File
@@ -435,55 +435,78 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
// @Param organization path string true "Organization ID" format(uuid)
// @Success 200 {array} codersdk.Template
// @Router /organizations/{organization}/templates [get]
func (api *API) templatesByOrganization(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
organization := httpmw.OrganizationParam(r)
func (api *API) templatesByOrganization() http.HandlerFunc {
// TODO: Should deprecate this endpoint and make it akin to /workspaces with
// a filter. There isn't a need to make the organization filter argument
// part of the query url.
// mutate the filter to only include templates from the given organization.
return api.fetchTemplates(func(r *http.Request, arg *database.GetTemplatesWithFilterParams) {
organization := httpmw.OrganizationParam(r)
arg.OrganizationID = organization.ID
})
}
p := httpapi.NewQueryParamParser()
values := r.URL.Query()
// @Summary Get all templates
// @ID get-all-templates
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Success 200 {array} codersdk.Template
// @Router /templates [get]
func (api *API) fetchTemplates(mutate func(r *http.Request, arg *database.GetTemplatesWithFilterParams)) http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
deprecated := sql.NullBool{}
if values.Has("deprecated") {
deprecated = sql.NullBool{
Bool: p.Boolean(values, false, "deprecated"),
Valid: true,
p := httpapi.NewQueryParamParser()
values := r.URL.Query()
deprecated := sql.NullBool{}
if values.Has("deprecated") {
deprecated = sql.NullBool{
Bool: p.Boolean(values, false, "deprecated"),
Valid: true,
}
}
if len(p.Errors) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid query params.",
Validations: p.Errors,
})
return
}
}
if len(p.Errors) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid query params.",
Validations: p.Errors,
})
return
}
prepared, err := api.HTTPAuth.AuthorizeSQLFilter(r, policy.ActionRead, rbac.ResourceTemplate.Type)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error preparing sql filter.",
Detail: err.Error(),
})
return
}
prepared, err := api.HTTPAuth.AuthorizeSQLFilter(r, policy.ActionRead, rbac.ResourceTemplate.Type)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error preparing sql filter.",
Detail: err.Error(),
})
return
}
// Filter templates based on rbac permissions
templates, err := api.Database.GetAuthorizedTemplates(ctx, database.GetTemplatesWithFilterParams{
OrganizationID: organization.ID,
Deprecated: deprecated,
}, prepared)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
args := database.GetTemplatesWithFilterParams{
Deprecated: deprecated,
}
if mutate != nil {
mutate(r, &args)
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching templates in organization.",
Detail: err.Error(),
})
return
}
// Filter templates based on rbac permissions
templates, err := api.Database.GetAuthorizedTemplates(ctx, args, prepared)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplates(templates))
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching templates in organization.",
Detail: err.Error(),
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplates(templates))
}
}
// @Summary Get templates by organization and template name
+36
View File
@@ -438,6 +438,42 @@ func TestTemplatesByOrganization(t *testing.T) {
templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID)
require.NoError(t, err)
require.Len(t, templates, 2)
// Listing all should match
templates, err = client.Templates(ctx)
require.NoError(t, err)
require.Len(t, templates, 2)
})
t.Run("MultipleOrganizations", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, client)
org2 := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{})
user, _ := coderdtest.CreateAnotherUser(t, client, org2.ID)
// 2 templates in first organization
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
coderdtest.CreateTemplate(t, client, owner.OrganizationID, version2.ID)
// 2 in the second organization
version3 := coderdtest.CreateTemplateVersion(t, client, org2.ID, nil)
version4 := coderdtest.CreateTemplateVersion(t, client, org2.ID, nil)
coderdtest.CreateTemplate(t, client, org2.ID, version3.ID)
coderdtest.CreateTemplate(t, client, org2.ID, version4.ID)
ctx := testutil.Context(t, testutil.WaitLong)
// All 4 are viewable by the owner
templates, err := client.Templates(ctx)
require.NoError(t, err)
require.Len(t, templates, 4)
// Only 2 are viewable by the org user
templates, err = user.Templates(ctx)
require.NoError(t, err)
require.Len(t, templates, 2)
})
}
+19
View File
@@ -362,6 +362,25 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui
return templates, json.NewDecoder(res.Body).Decode(&templates)
}
// Templates lists all viewable templates
func (c *Client) Templates(ctx context.Context) ([]Template, error) {
res, err := c.Request(ctx, http.MethodGet,
"/api/v2/templates",
nil,
)
if err != nil {
return nil, xerrors.Errorf("execute request: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, ReadBodyAsError(res)
}
var templates []Template
return templates, json.NewDecoder(res.Body).Decode(&templates)
}
// TemplateByName finds a template inside the organization provided with a case-insensitive name.
func (c *Client) TemplateByName(ctx context.Context, organizationID uuid.UUID, name string) (Template, error) {
if name == "" {
+126
View File
@@ -617,6 +617,132 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get all templates
### Code samples
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/templates \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`GET /templates`
### Example responses
> 200 Response
```json
[
{
"active_user_count": 0,
"active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc",
"activity_bump_ms": 0,
"allow_user_autostart": true,
"allow_user_autostop": true,
"allow_user_cancel_workspace_jobs": true,
"autostart_requirement": {
"days_of_week": ["monday"]
},
"autostop_requirement": {
"days_of_week": ["monday"],
"weeks": 0
},
"build_time_stats": {
"property1": {
"p50": 123,
"p95": 146
},
"property2": {
"p50": 123,
"p95": 146
}
},
"created_at": "2019-08-24T14:15:22Z",
"created_by_id": "9377d689-01fb-4abf-8450-3368d2c1924f",
"created_by_name": "string",
"default_ttl_ms": 0,
"deprecated": true,
"deprecation_message": "string",
"description": "string",
"display_name": "string",
"failure_ttl_ms": 0,
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"max_port_share_level": "owner",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"provisioner": "terraform",
"require_active_version": true,
"time_til_dormant_autodelete_ms": 0,
"time_til_dormant_ms": 0,
"updated_at": "2019-08-24T14:15:22Z"
}
]
```
### Responses
| Status | Meaning | Description | Schema |
| ------ | ------------------------------------------------------- | ----------- | --------------------------------------------------------- |
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.Template](schemas.md#codersdktemplate) |
<h3 id="get-all-templates-responseschema">Response Schema</h3>
Status Code **200**
| Name | Type | Required | Restrictions | Description |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `[array item]` | array | false | | |
| `» active_user_count` | integer | false | | Active user count is set to -1 when loading. |
| `» active_version_id` | string(uuid) | false | | |
| `» activity_bump_ms` | integer | false | | |
| `» allow_user_autostart` | boolean | false | | Allow user autostart and AllowUserAutostop are enterprise-only. Their values are only used if your license is entitled to use the advanced template scheduling feature. |
| `» allow_user_autostop` | boolean | false | | |
| `» allow_user_cancel_workspace_jobs` | boolean | false | | |
| `» autostart_requirement` | [codersdk.TemplateAutostartRequirement](schemas.md#codersdktemplateautostartrequirement) | false | | |
| `»» days_of_week` | array | false | | Days of week is a list of days of the week in which autostart is allowed to happen. If no days are specified, autostart is not allowed. |
| `» autostop_requirement` | [codersdk.TemplateAutostopRequirement](schemas.md#codersdktemplateautostoprequirement) | false | | Autostop requirement and AutostartRequirement are enterprise features. Its value is only used if your license is entitled to use the advanced template scheduling feature. |
| `»» days_of_week` | array | false | | Days of week is a list of days of the week on which restarts are required. Restarts happen within the user's quiet hours (in their configured timezone). If no days are specified, restarts are not required. Weekdays cannot be specified twice. |
| Restarts will only happen on weekdays in this list on weeks which line up with Weeks. |
| `»» weeks` | integer | false | | Weeks is the number of weeks between required restarts. Weeks are synced across all workspaces (and Coder deployments) using modulo math on a hardcoded epoch week of January 2nd, 2023 (the first Monday of 2023). Values of 0 or 1 indicate weekly restarts. Values of 2 indicate fortnightly restarts, etc. |
| `» build_time_stats` | [codersdk.TemplateBuildTimeStats](schemas.md#codersdktemplatebuildtimestats) | false | | |
| `»» [any property]` | [codersdk.TransitionStats](schemas.md#codersdktransitionstats) | false | | |
| `»»» p50` | integer | false | | |
| `»»» p95` | integer | false | | |
| `» created_at` | string(date-time) | false | | |
| `» created_by_id` | string(uuid) | false | | |
| `» created_by_name` | string | false | | |
| `» default_ttl_ms` | integer | false | | |
| `» deprecated` | boolean | false | | |
| `» deprecation_message` | string | false | | |
| `» description` | string | false | | |
| `» display_name` | string | false | | |
| `» failure_ttl_ms` | integer | false | | Failure ttl ms TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. |
| `» icon` | string | false | | |
| `» id` | string(uuid) | false | | |
| `» max_port_share_level` | [codersdk.WorkspaceAgentPortShareLevel](schemas.md#codersdkworkspaceagentportsharelevel) | false | | |
| `» name` | string | false | | |
| `» organization_id` | string(uuid) | false | | |
| `» provisioner` | string | false | | |
| `» require_active_version` | boolean | false | | Require active version mandates that workspaces are built with the active template version. |
| `» time_til_dormant_autodelete_ms` | integer | false | | |
| `» time_til_dormant_ms` | integer | false | | |
| `» updated_at` | string(date-time) | false | | |
#### Enumerated Values
| Property | Value |
| ---------------------- | --------------- |
| `max_port_share_level` | `owner` |
| `max_port_share_level` | `authenticated` |
| `max_port_share_level` | `public` |
| `provisioner` | `terraform` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get template metadata by ID
### Code samples