mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: implement fetch all authorized templates api (#13678)
This commit is contained in:
Generated
+28
@@ -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": [
|
||||
|
||||
Generated
+24
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
Generated
+126
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user