From a5e8911d6795dbb7a13ef3d723e84768f69f899d Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 2 Feb 2023 15:47:53 -0600 Subject: [PATCH] fix: index template versions by template and name (#5993) * fix: index template versions by template and name We were incorrectly returning template versions by name relative to organizations. This could result in an incorrect version being returned if multiple templates had versions with the same name. * Fix auth referencing * Fix route location * Fix authorize route name * Fix previous call * Fix authorize route name --- coderd/apidoc/docs.go | 182 ++++++----- coderd/apidoc/swagger.json | 166 +++++----- coderd/coderd.go | 14 +- coderd/coderdtest/authorize.go | 18 +- coderd/database/databasefake/databasefake.go | 20 -- coderd/database/querier.go | 1 - coderd/database/queries.sql.go | 32 -- coderd/database/queries/templateversions.sql | 9 - coderd/templateversions.go | 83 ++++- coderd/templateversions_test.go | 14 +- codersdk/organizations.go | 4 +- codersdk/templateversions.go | 4 +- docs/api/templates.md | 302 +++++++++--------- site/src/api/api.ts | 6 +- .../TemplateVersionPage.tsx | 2 +- site/src/testHelpers/handlers.ts | 4 +- .../templateVersionXService.ts | 7 +- 17 files changed, 451 insertions(+), 417 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index fb5236fe61..6bb45c4fb3 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1330,6 +1330,104 @@ const docTemplate = `{ } } }, + "/organizations/{organization}/templates/{templatename}/versions/{templateversionname}": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Templates" + ], + "summary": "Get template version by organization, template, and name", + "operationId": "get-template-version-by-organization-template-and-name", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Template name", + "name": "templatename", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Template version name", + "name": "templateversionname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.TemplateVersion" + } + } + } + } + }, + "/organizations/{organization}/templates/{templatename}/versions/{templateversionname}/previous": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Templates" + ], + "summary": "Get previous template version by organization, template, and name", + "operationId": "get-previous-template-version-by-organization-template-and-name", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Template name", + "name": "templatename", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Template version name", + "name": "templateversionname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.TemplateVersion" + } + } + } + } + }, "/organizations/{organization}/templateversions": { "post": { "security": [ @@ -1377,90 +1475,6 @@ const docTemplate = `{ } } }, - "/organizations/{organization}/templateversions/{templateversionname}": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Templates" - ], - "summary": "Get template version by organization and name", - "operationId": "get-template-version-by-organization-and-name", - "parameters": [ - { - "type": "string", - "format": "uuid", - "description": "Organization ID", - "name": "organization", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Template version name", - "name": "templateversionname", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.TemplateVersion" - } - } - } - } - }, - "/organizations/{organization}/templateversions/{templateversionname}/previous": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Templates" - ], - "summary": "Get previous template version by organization and name", - "operationId": "get-previous-template-version-by-organization-and-name", - "parameters": [ - { - "type": "string", - "format": "uuid", - "description": "Organization ID", - "name": "organization", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Template version name", - "name": "templateversionname", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.TemplateVersion" - } - } - } - } - }, "/parameters/{scope}/{id}": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 0d6c224dbc..91c1d15ff2 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1160,6 +1160,96 @@ } } }, + "/organizations/{organization}/templates/{templatename}/versions/{templateversionname}": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Templates"], + "summary": "Get template version by organization, template, and name", + "operationId": "get-template-version-by-organization-template-and-name", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Template name", + "name": "templatename", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Template version name", + "name": "templateversionname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.TemplateVersion" + } + } + } + } + }, + "/organizations/{organization}/templates/{templatename}/versions/{templateversionname}/previous": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Templates"], + "summary": "Get previous template version by organization, template, and name", + "operationId": "get-previous-template-version-by-organization-template-and-name", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Template name", + "name": "templatename", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Template version name", + "name": "templateversionname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.TemplateVersion" + } + } + } + } + }, "/organizations/{organization}/templateversions": { "post": { "security": [ @@ -1201,82 +1291,6 @@ } } }, - "/organizations/{organization}/templateversions/{templateversionname}": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": ["application/json"], - "tags": ["Templates"], - "summary": "Get template version by organization and name", - "operationId": "get-template-version-by-organization-and-name", - "parameters": [ - { - "type": "string", - "format": "uuid", - "description": "Organization ID", - "name": "organization", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Template version name", - "name": "templateversionname", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.TemplateVersion" - } - } - } - } - }, - "/organizations/{organization}/templateversions/{templateversionname}/previous": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": ["application/json"], - "tags": ["Templates"], - "summary": "Get previous template version by organization and name", - "operationId": "get-previous-template-version-by-organization-and-name", - "parameters": [ - { - "type": "string", - "format": "uuid", - "description": "Organization ID", - "name": "organization", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Template version name", - "name": "templateversionname", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.TemplateVersion" - } - } - } - } - }, "/parameters/{scope}/{id}": { "get": { "security": [ diff --git a/coderd/coderd.go b/coderd/coderd.go index 62ffe040ad..2b16094d2d 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -398,16 +398,18 @@ func New(options *Options) *API { httpmw.ExtractOrganizationParam(options.Database), ) r.Get("/", api.organization) - r.Route("/templateversions", func(r chi.Router) { - r.Post("/", api.postTemplateVersionsByOrganization) - r.Get("/{templateversionname}", api.templateVersionByOrganizationAndName) - r.Get("/{templateversionname}/previous", api.previousTemplateVersionByOrganizationAndName) - }) + r.Post("/templateversions", api.postTemplateVersionsByOrganization) r.Route("/templates", func(r chi.Router) { r.Post("/", api.postTemplateByOrganization) r.Get("/", api.templatesByOrganization) - r.Get("/{templatename}", api.templateByOrganizationAndName) r.Get("/examples", api.templateExamples) + r.Route("/{templatename}", func(r chi.Router) { + r.Get("/", api.templateByOrganizationAndName) + r.Route("/versions/{templateversionname}", func(r chi.Router) { + r.Get("/", api.templateVersionByOrganizationTemplateAndName) + r.Get("/previous", api.previousTemplateVersionByOrganizationTemplateAndName) + }) + }) }) r.Route("/members", func(r chi.Router) { r.Get("/roles", api.assignableOrgRoles) diff --git a/coderd/coderdtest/authorize.go b/coderd/coderdtest/authorize.go index 14019815d2..0a95e8bb52 100644 --- a/coderd/coderdtest/authorize.go +++ b/coderd/coderdtest/authorize.go @@ -239,6 +239,14 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) { AssertAction: rbac.ActionRead, AssertObject: templateObj, }, + "GET:/api/v2/organizations/{organization}/templates/{templatename}/versions/{templateversionname}": { + AssertAction: rbac.ActionRead, + AssertObject: templateObj, + }, + "GET:/api/v2/organizations/{organization}/templates/{templatename}/versions/{templateversionname}/previous": { + AssertAction: rbac.ActionRead, + AssertObject: templateObj, + }, "POST:/api/v2/organizations/{organization}/members/{user}/workspaces": { AssertAction: rbac.ActionCreate, // No ID when creating @@ -252,12 +260,10 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) { "GET:/api/v2/applications/auth-redirect": {AssertAction: rbac.ActionCreate, AssertObject: rbac.ResourceAPIKey}, // These endpoints need payloads to get to the auth part. Payloads will be required - "PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true}, - "PUT:/api/v2/organizations/{organization}/members/{user}/roles": {NoAuthorize: true}, - "POST:/api/v2/workspaces/{workspace}/builds": {StatusCode: http.StatusBadRequest, NoAuthorize: true}, - "POST:/api/v2/organizations/{organization}/templateversions": {StatusCode: http.StatusBadRequest, NoAuthorize: true}, - "GET:/api/v2/organizations/{organization}/templateversions/{templateversionname}": {StatusCode: http.StatusBadRequest, NoAuthorize: true}, - "GET:/api/v2/organizations/{organization}/templateversions/{templateversionname}/previous": {StatusCode: http.StatusBadRequest, NoAuthorize: true}, + "PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true}, + "PUT:/api/v2/organizations/{organization}/members/{user}/roles": {NoAuthorize: true}, + "POST:/api/v2/workspaces/{workspace}/builds": {StatusCode: http.StatusBadRequest, NoAuthorize: true}, + "POST:/api/v2/organizations/{organization}/templateversions": {StatusCode: http.StatusBadRequest, NoAuthorize: true}, // Endpoints that use the SQLQuery filter. "GET:/api/v2/workspaces/": { diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 4c2fa4bbcd..f3d7b4a6f3 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -1797,26 +1797,6 @@ func (q *fakeQuerier) GetTemplateVersionParameters(_ context.Context, templateVe return parameters, nil } -func (q *fakeQuerier) GetTemplateVersionByOrganizationAndName(_ context.Context, arg database.GetTemplateVersionByOrganizationAndNameParams) (database.TemplateVersion, error) { - if err := validateDatabaseType(arg); err != nil { - return database.TemplateVersion{}, err - } - - q.mutex.RLock() - defer q.mutex.RUnlock() - - for _, templateVersion := range q.templateVersions { - if templateVersion.OrganizationID != arg.OrganizationID { - continue - } - if !strings.EqualFold(templateVersion.Name, arg.Name) { - continue - } - return templateVersion, nil - } - return database.TemplateVersion{}, sql.ErrNoRows -} - func (q *fakeQuerier) GetTemplateVersionByID(_ context.Context, templateVersionID uuid.UUID) (database.TemplateVersion, error) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 4795250699..38b5b34c6a 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -84,7 +84,6 @@ type sqlcQuerier interface { GetTemplateDAUs(ctx context.Context, templateID uuid.UUID) ([]GetTemplateDAUsRow, error) GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (TemplateVersion, error) GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error) - GetTemplateVersionByOrganizationAndName(ctx context.Context, arg GetTemplateVersionByOrganizationAndNameParams) (TemplateVersion, error) GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg GetTemplateVersionByTemplateIDAndNameParams) (TemplateVersion, error) GetTemplateVersionParameters(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionParameter, error) GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UUID) ([]TemplateVersion, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index d7ac44430a..7b2b77f753 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3731,38 +3731,6 @@ func (q *sqlQuerier) GetTemplateVersionByJobID(ctx context.Context, jobID uuid.U return i, err } -const getTemplateVersionByOrganizationAndName = `-- name: GetTemplateVersionByOrganizationAndName :one -SELECT - id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by -FROM - template_versions -WHERE - organization_id = $1 - AND "name" = $2 -` - -type GetTemplateVersionByOrganizationAndNameParams struct { - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - Name string `db:"name" json:"name"` -} - -func (q *sqlQuerier) GetTemplateVersionByOrganizationAndName(ctx context.Context, arg GetTemplateVersionByOrganizationAndNameParams) (TemplateVersion, error) { - row := q.db.QueryRowContext(ctx, getTemplateVersionByOrganizationAndName, arg.OrganizationID, arg.Name) - var i TemplateVersion - err := row.Scan( - &i.ID, - &i.TemplateID, - &i.OrganizationID, - &i.CreatedAt, - &i.UpdatedAt, - &i.Name, - &i.Readme, - &i.JobID, - &i.CreatedBy, - ) - return i, err -} - const getTemplateVersionByTemplateIDAndName = `-- name: GetTemplateVersionByTemplateIDAndName :one SELECT id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by diff --git a/coderd/database/queries/templateversions.sql b/coderd/database/queries/templateversions.sql index d49d86bf56..78dcff44e2 100644 --- a/coderd/database/queries/templateversions.sql +++ b/coderd/database/queries/templateversions.sql @@ -52,15 +52,6 @@ WHERE template_id = $1 AND "name" = $2; --- name: GetTemplateVersionByOrganizationAndName :one -SELECT - * -FROM - template_versions -WHERE - organization_id = $1 - AND "name" = $2; - -- name: GetTemplateVersionByID :one SELECT * diff --git a/coderd/templateversions.go b/coderd/templateversions.go index abcf87ce1e..f013912521 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -763,22 +763,50 @@ func (api *API) templateVersionByName(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(job), user)) } -// @Summary Get template version by organization and name -// @ID get-template-version-by-organization-and-name +// @Summary Get template version by organization, template, and name +// @ID get-template-version-by-organization-template-and-name // @Security CoderSessionToken // @Produce json // @Tags Templates // @Param organization path string true "Organization ID" format(uuid) +// @Param templatename path string true "Template name" // @Param templateversionname path string true "Template version name" // @Success 200 {object} codersdk.TemplateVersion -// @Router /organizations/{organization}/templateversions/{templateversionname} [get] -func (api *API) templateVersionByOrganizationAndName(rw http.ResponseWriter, r *http.Request) { +// @Router /organizations/{organization}/templates/{templatename}/versions/{templateversionname} [get] +func (api *API) templateVersionByOrganizationTemplateAndName(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() organization := httpmw.OrganizationParam(r) - templateVersionName := chi.URLParam(r, "templateversionname") - templateVersion, err := api.Database.GetTemplateVersionByOrganizationAndName(ctx, database.GetTemplateVersionByOrganizationAndNameParams{ + templateName := chi.URLParam(r, "templatename") + + template, err := api.Database.GetTemplateByOrganizationAndName(ctx, database.GetTemplateByOrganizationAndNameParams{ OrganizationID: organization.ID, - Name: templateVersionName, + Name: templateName, + }) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + httpapi.ResourceNotFound(rw) + return + } + + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching template.", + Detail: err.Error(), + }) + return + } + + if !api.Authorize(r, rbac.ActionRead, template) { + httpapi.ResourceNotFound(rw) + return + } + + templateVersionName := chi.URLParam(r, "templateversionname") + templateVersion, err := api.Database.GetTemplateVersionByTemplateIDAndName(ctx, database.GetTemplateVersionByTemplateIDAndNameParams{ + TemplateID: uuid.NullUUID{ + UUID: template.ID, + Valid: true, + }, + Name: templateVersionName, }) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ @@ -814,22 +842,49 @@ func (api *API) templateVersionByOrganizationAndName(rw http.ResponseWriter, r * httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(job), user)) } -// @Summary Get previous template version by organization and name -// @ID get-previous-template-version-by-organization-and-name +// @Summary Get previous template version by organization, template, and name +// @ID get-previous-template-version-by-organization-template-and-name // @Security CoderSessionToken // @Produce json // @Tags Templates // @Param organization path string true "Organization ID" format(uuid) +// @Param templatename path string true "Template name" // @Param templateversionname path string true "Template version name" // @Success 200 {object} codersdk.TemplateVersion -// @Router /organizations/{organization}/templateversions/{templateversionname}/previous [get] -func (api *API) previousTemplateVersionByOrganizationAndName(rw http.ResponseWriter, r *http.Request) { +// @Router /organizations/{organization}/templates/{templatename}/versions/{templateversionname}/previous [get] +func (api *API) previousTemplateVersionByOrganizationTemplateAndName(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() organization := httpmw.OrganizationParam(r) - templateVersionName := chi.URLParam(r, "templateversionname") - templateVersion, err := api.Database.GetTemplateVersionByOrganizationAndName(ctx, database.GetTemplateVersionByOrganizationAndNameParams{ + templateName := chi.URLParam(r, "templatename") + template, err := api.Database.GetTemplateByOrganizationAndName(ctx, database.GetTemplateByOrganizationAndNameParams{ OrganizationID: organization.ID, - Name: templateVersionName, + Name: templateName, + }) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + httpapi.ResourceNotFound(rw) + return + } + + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching template.", + Detail: err.Error(), + }) + return + } + + if !api.Authorize(r, rbac.ActionRead, template) { + httpapi.ResourceNotFound(rw) + return + } + + templateVersionName := chi.URLParam(r, "templateversionname") + templateVersion, err := api.Database.GetTemplateVersionByTemplateIDAndName(ctx, database.GetTemplateVersionByTemplateIDAndNameParams{ + TemplateID: uuid.NullUUID{ + UUID: template.ID, + Valid: true, + }, + Name: templateVersionName, }) if err != nil { if xerrors.Is(err, sql.ErrNoRows) { diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index adf35aeb1c..f178c613ce 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -992,19 +992,19 @@ func TestPaginatedTemplateVersions(t *testing.T) { } } -func TestTemplateVersionByOrganizationAndName(t *testing.T) { +func TestTemplateVersionByOrganizationTemplateAndName(t *testing.T) { t.Parallel() t.Run("NotFound", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - _, err := client.TemplateVersionByOrganizationAndName(ctx, user.OrganizationID, "nothing") + _, err := client.TemplateVersionByOrganizationAndName(ctx, user.OrganizationID, template.Name, "nothing") var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) @@ -1015,12 +1015,12 @@ func TestTemplateVersionByOrganizationAndName(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - _, err := client.TemplateVersionByOrganizationAndName(ctx, user.OrganizationID, version.Name) + _, err := client.TemplateVersionByOrganizationAndName(ctx, user.OrganizationID, template.Name, version.Name) require.NoError(t, err) }) } @@ -1048,7 +1048,7 @@ func TestPreviousTemplateVersion(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - _, err := client.PreviousTemplateVersion(ctx, user.OrganizationID, templateBVersion1.Name) + _, err := client.PreviousTemplateVersion(ctx, user.OrganizationID, templateB.Name, templateBVersion1.Name) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) @@ -1075,7 +1075,7 @@ func TestPreviousTemplateVersion(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - result, err := client.PreviousTemplateVersion(ctx, user.OrganizationID, templateBVersion2.Name) + result, err := client.PreviousTemplateVersion(ctx, user.OrganizationID, templateB.Name, templateBVersion2.Name) require.NoError(t, err) require.Equal(t, templateBVersion1.ID, result.ID) }) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index e4a07f6990..e7d1689240 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -153,9 +153,9 @@ func (c *Client) CreateTemplateVersion(ctx context.Context, organizationID uuid. return templateVersion, json.NewDecoder(res.Body).Decode(&templateVersion) } -func (c *Client) TemplateVersionByOrganizationAndName(ctx context.Context, organizationID uuid.UUID, name string) (TemplateVersion, error) { +func (c *Client) TemplateVersionByOrganizationAndName(ctx context.Context, organizationID uuid.UUID, templateName, versionName string) (TemplateVersion, error) { res, err := c.Request(ctx, http.MethodGet, - fmt.Sprintf("/api/v2/organizations/%s/templateversions/%s", organizationID.String(), name), + fmt.Sprintf("/api/v2/organizations/%s/templates/%s/versions/%s", organizationID.String(), templateName, versionName), nil, ) diff --git a/codersdk/templateversions.go b/codersdk/templateversions.go index f7b456779c..55feefca30 100644 --- a/codersdk/templateversions.go +++ b/codersdk/templateversions.go @@ -221,8 +221,8 @@ func (c *Client) CancelTemplateVersionDryRun(ctx context.Context, version, job u return nil } -func (c *Client) PreviousTemplateVersion(ctx context.Context, organization uuid.UUID, versionName string) (TemplateVersion, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/templateversions/%s/previous", organization, versionName), nil) +func (c *Client) PreviousTemplateVersion(ctx context.Context, organization uuid.UUID, templateName, versionName string) (TemplateVersion, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/templates/%s/versions/%s/previous", organization, templateName, versionName), nil) if err != nil { return TemplateVersion{}, err } diff --git a/docs/api/templates.md b/docs/api/templates.md index 2d62e30f5c..859618198a 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -375,6 +375,158 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get template version by organization, template, and name + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templates/{templatename}/versions/{templateversionname} \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /organizations/{organization}/templates/{templatename}/versions/{templateversionname}` + +### Parameters + +| Name | In | Type | Required | Description | +| --------------------- | ---- | ------------ | -------- | --------------------- | +| `organization` | path | string(uuid) | true | Organization ID | +| `templatename` | path | string | true | Template name | +| `templateversionname` | path | string | true | Template version name | + +### Example responses + +> 200 Response + +```json +{ + "created_at": "2019-08-24T14:15:22Z", + "created_by": { + "avatar_url": "http://example.com", + "created_at": "2019-08-24T14:15:22Z", + "email": "user@example.com", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "last_seen_at": "2019-08-24T14:15:22Z", + "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "roles": [ + { + "display_name": "string", + "name": "string" + } + ], + "status": "active", + "username": "string" + }, + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "job": { + "canceled_at": "2019-08-24T14:15:22Z", + "completed_at": "2019-08-24T14:15:22Z", + "created_at": "2019-08-24T14:15:22Z", + "error": "string", + "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "started_at": "2019-08-24T14:15:22Z", + "status": "pending", + "tags": { + "property1": "string", + "property2": "string" + }, + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + }, + "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "readme": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "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 | [codersdk.TemplateVersion](schemas.md#codersdktemplateversion) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Get previous template version by organization, template, and name + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templates/{templatename}/versions/{templateversionname}/previous \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /organizations/{organization}/templates/{templatename}/versions/{templateversionname}/previous` + +### Parameters + +| Name | In | Type | Required | Description | +| --------------------- | ---- | ------------ | -------- | --------------------- | +| `organization` | path | string(uuid) | true | Organization ID | +| `templatename` | path | string | true | Template name | +| `templateversionname` | path | string | true | Template version name | + +### Example responses + +> 200 Response + +```json +{ + "created_at": "2019-08-24T14:15:22Z", + "created_by": { + "avatar_url": "http://example.com", + "created_at": "2019-08-24T14:15:22Z", + "email": "user@example.com", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "last_seen_at": "2019-08-24T14:15:22Z", + "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "roles": [ + { + "display_name": "string", + "name": "string" + } + ], + "status": "active", + "username": "string" + }, + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "job": { + "canceled_at": "2019-08-24T14:15:22Z", + "completed_at": "2019-08-24T14:15:22Z", + "created_at": "2019-08-24T14:15:22Z", + "error": "string", + "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "started_at": "2019-08-24T14:15:22Z", + "status": "pending", + "tags": { + "property1": "string", + "property2": "string" + }, + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + }, + "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "readme": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "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 | [codersdk.TemplateVersion](schemas.md#codersdktemplateversion) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Create template version by organization ### Code samples @@ -474,156 +626,6 @@ 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 template version by organization and name - -### Code samples - -```shell -# Example request using curl -curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templateversions/{templateversionname} \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`GET /organizations/{organization}/templateversions/{templateversionname}` - -### Parameters - -| Name | In | Type | Required | Description | -| --------------------- | ---- | ------------ | -------- | --------------------- | -| `organization` | path | string(uuid) | true | Organization ID | -| `templateversionname` | path | string | true | Template version name | - -### Example responses - -> 200 Response - -```json -{ - "created_at": "2019-08-24T14:15:22Z", - "created_by": { - "avatar_url": "http://example.com", - "created_at": "2019-08-24T14:15:22Z", - "email": "user@example.com", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "last_seen_at": "2019-08-24T14:15:22Z", - "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], - "roles": [ - { - "display_name": "string", - "name": "string" - } - ], - "status": "active", - "username": "string" - }, - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "job": { - "canceled_at": "2019-08-24T14:15:22Z", - "completed_at": "2019-08-24T14:15:22Z", - "created_at": "2019-08-24T14:15:22Z", - "error": "string", - "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "started_at": "2019-08-24T14:15:22Z", - "status": "pending", - "tags": { - "property1": "string", - "property2": "string" - }, - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" - }, - "name": "string", - "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "readme": "string", - "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", - "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 | [codersdk.TemplateVersion](schemas.md#codersdktemplateversion) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - -## Get previous template version by organization and name - -### Code samples - -```shell -# Example request using curl -curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templateversions/{templateversionname}/previous \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`GET /organizations/{organization}/templateversions/{templateversionname}/previous` - -### Parameters - -| Name | In | Type | Required | Description | -| --------------------- | ---- | ------------ | -------- | --------------------- | -| `organization` | path | string(uuid) | true | Organization ID | -| `templateversionname` | path | string | true | Template version name | - -### Example responses - -> 200 Response - -```json -{ - "created_at": "2019-08-24T14:15:22Z", - "created_by": { - "avatar_url": "http://example.com", - "created_at": "2019-08-24T14:15:22Z", - "email": "user@example.com", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "last_seen_at": "2019-08-24T14:15:22Z", - "organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], - "roles": [ - { - "display_name": "string", - "name": "string" - } - ], - "status": "active", - "username": "string" - }, - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "job": { - "canceled_at": "2019-08-24T14:15:22Z", - "completed_at": "2019-08-24T14:15:22Z", - "created_at": "2019-08-24T14:15:22Z", - "error": "string", - "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "started_at": "2019-08-24T14:15:22Z", - "status": "pending", - "tags": { - "property1": "string", - "property2": "string" - }, - "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" - }, - "name": "string", - "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "readme": "string", - "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", - "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 | [codersdk.TemplateVersion](schemas.md#codersdktemplateversion) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - ## Get template metadata by ID ### Code samples diff --git a/site/src/api/api.ts b/site/src/api/api.ts index dd651a54ae..2007b40a36 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -231,10 +231,11 @@ export const getTemplateVersions = async ( export const getTemplateVersionByName = async ( organizationId: string, + templateName: string, versionName: string, ): Promise => { const response = await axios.get( - `/api/v2/organizations/${organizationId}/templateversions/${versionName}`, + `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}`, ) return response.data } @@ -245,11 +246,12 @@ export type GetPreviousTemplateVersionByNameResponse = export const getPreviousTemplateVersionByName = async ( organizationId: string, + templateName: string, versionName: string, ): Promise => { try { const response = await axios.get( - `/api/v2/organizations/${organizationId}/templateversions/${versionName}/previous`, + `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}/previous`, ) return response.data } catch (error) { diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx index 0f2b40d37c..ed836d58fd 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx @@ -18,7 +18,7 @@ export const TemplateVersionPage: FC = () => { const { version: versionName, template: templateName } = useParams() as Params const orgId = useOrganizationId() const [state] = useMachine(templateVersionMachine, { - context: { versionName, orgId }, + context: { templateName, versionName, orgId }, }) const tab = useTab("file", "0") const { t } = useTranslation("templateVersionPage") diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 9b11058004..0dd0f2d0fe 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -90,13 +90,13 @@ export const handlers = [ }, ), rest.get( - "api/v2/organizations/:organizationId/templateversions/:templateVersionName", + "api/v2/organizations/:organizationId/templates/:templateName/versions/:templateVersionName", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockTemplateVersion)) }, ), rest.get( - "api/v2/organizations/:organizationId/templateversions/:templateVersionName/previous", + "api/v2/organizations/:organizationId/templates/:templateName/versions/:templateVersionName/previous", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockTemplateVersion2)) }, diff --git a/site/src/xServices/templateVersion/templateVersionXService.ts b/site/src/xServices/templateVersion/templateVersionXService.ts index 73cd0989bf..1c59f792e2 100644 --- a/site/src/xServices/templateVersion/templateVersionXService.ts +++ b/site/src/xServices/templateVersion/templateVersionXService.ts @@ -12,6 +12,7 @@ import { assign, createMachine } from "xstate" export interface TemplateVersionMachineContext { orgId: string + templateName: string versionName: string currentVersion?: TemplateVersion currentFiles?: TemplateVersionFiles @@ -94,10 +95,10 @@ export const templateVersionMachine = createMachine( }), }, services: { - loadVersions: async ({ orgId, versionName }) => { + loadVersions: async ({ orgId, templateName, versionName }) => { const [currentVersion, previousVersion] = await Promise.all([ - getTemplateVersionByName(orgId, versionName), - getPreviousTemplateVersionByName(orgId, versionName), + getTemplateVersionByName(orgId, templateName, versionName), + getPreviousTemplateVersionByName(orgId, templateName, versionName), ]) return {