From 300a763560cf37f80cf036e6c54cc9884e89eace Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 6 Dec 2022 21:42:24 +0000 Subject: [PATCH 1/9] feat: add examples to api --- cli/templateinit.go | 3 ++- coderd/coderd.go | 1 + coderd/users.go | 15 +++++++++++++++ codersdk/organizations.go | 3 ++- codersdk/templates.go | 8 ++++++++ examples/examples.go | 18 ++++++------------ site/src/api/typesGenerated.ts | 12 +++++++++++- 7 files changed, 45 insertions(+), 15 deletions(-) diff --git a/cli/templateinit.go b/cli/templateinit.go index ad936024a9f11..39a8f72662bde 100644 --- a/cli/templateinit.go +++ b/cli/templateinit.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" "github.com/coder/coder/cli/cliui" + "github.com/coder/coder/codersdk" "github.com/coder/coder/examples" "github.com/coder/coder/provisionersdk" ) @@ -22,7 +23,7 @@ func templateInit() *cobra.Command { return err } exampleNames := []string{} - exampleByName := map[string]examples.Example{} + exampleByName := map[string]codersdk.TemplateExample{} for _, example := range exampleList { name := fmt.Sprintf( "%s\n%s\n%s\n", diff --git a/coderd/coderd.go b/coderd/coderd.go index fb36deeec42c3..675d023e82194 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -355,6 +355,7 @@ func New(options *Options) *API { r.Post("/", api.postTemplateByOrganization) r.Get("/", api.templatesByOrganization) r.Get("/{templatename}", api.templateByOrganizationAndName) + r.Get("/examples", api.templateExamples) }) r.Route("/members", func(r chi.Router) { r.Get("/roles", api.assignableOrgRoles) diff --git a/coderd/users.go b/coderd/users.go index b3e42cba75c91..7bf27ce94c9de 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -825,6 +825,21 @@ func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, convertUser(updatedUser, organizationIDs)) } +func (_ *API) templateExamples(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + ex, err := examples.List() + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching examples.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, ex) +} + // updateSiteUserRoles will ensure only site wide roles are passed in as arguments. // If an organization role is included, an error is returned. func (api *API) updateSiteUserRoles(ctx context.Context, args database.UpdateUserRolesParams) (database.User, error) { diff --git a/codersdk/organizations.go b/codersdk/organizations.go index c61161abd7dcf..2b9f7e906d4a5 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -38,7 +38,8 @@ type CreateTemplateVersionRequest struct { // TemplateID optionally associates a version with a template. TemplateID uuid.UUID `json:"template_id,omitempty"` StorageMethod ProvisionerStorageMethod `json:"storage_method" validate:"oneof=file,required"` - FileID uuid.UUID `json:"file_id" validate:"required"` + FileID uuid.UUID `json:"file_id,omitempty" validate:"required_without=ExampleID"` + ExampleID uuid.UUID `json:"example_id,omitempty" validate:"required_without=FileID"` Provisioner ProvisionerType `json:"provisioner" validate:"oneof=terraform echo,required"` ProvisionerTags map[string]string `json:"tags"` diff --git a/codersdk/templates.go b/codersdk/templates.go index 9a6b6d8ec4c58..e042a056a6e70 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -82,6 +82,14 @@ type UpdateTemplateMeta struct { AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs,omitempty"` } +type TemplateExample struct { + ID string `json:"id"` + URL string `json:"url"` + Name string `json:"name"` + Description string `json:"description"` + Markdown string `json:"markdown"` +} + // Template returns a single template. func (c *Client) Template(ctx context.Context, template uuid.UUID) (Template, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s", template), nil) diff --git a/examples/examples.go b/examples/examples.go index 62c08d46b15af..d34169d8bd489 100644 --- a/examples/examples.go +++ b/examples/examples.go @@ -12,6 +12,8 @@ import ( "github.com/gohugoio/hugo/parser/pageparser" "golang.org/x/sync/singleflight" "golang.org/x/xerrors" + + "github.com/coder/coder/codersdk" ) var ( @@ -19,23 +21,15 @@ var ( files embed.FS exampleBasePath = "https://github.com/coder/coder/tree/main/examples/templates/" - examples = make([]Example, 0) + examples = make([]codersdk.TemplateExample, 0) parseExamples sync.Once archives = singleflight.Group{} ) -type Example struct { - ID string `json:"id"` - URL string `json:"url"` - Name string `json:"name"` - Description string `json:"description"` - Markdown string `json:"markdown"` -} - const rootDir = "templates" // List returns all embedded examples. -func List() ([]Example, error) { +func List() ([]codersdk.TemplateExample, error) { var returnError error parseExamples.Do(func() { files, err := fs.Sub(files, rootDir) @@ -92,7 +86,7 @@ func List() ([]Example, error) { return } - examples = append(examples, Example{ + examples = append(examples, codersdk.TemplateExample{ ID: exampleID, URL: exampleURL, Name: name, @@ -112,7 +106,7 @@ func Archive(exampleID string) ([]byte, error) { return nil, xerrors.Errorf("list: %w", err) } - var selected Example + var selected codersdk.TemplateExample for _, example := range examples { if example.ID != exampleID { continue diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index f18086fa775da..345dc6fa7d819 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -189,7 +189,8 @@ export interface CreateTemplateVersionRequest { readonly name?: string readonly template_id?: string readonly storage_method: ProvisionerStorageMethod - readonly file_id: string + readonly file_id?: string + readonly example_id?: string readonly provisioner: ProvisionerType readonly tags: Record readonly parameter_values?: CreateParameterRequest[] @@ -668,6 +669,15 @@ export interface TemplateDAUsResponse { readonly entries: DAUEntry[] } +// From codersdk/templates.go +export interface TemplateExample { + readonly id: string + readonly url: string + readonly name: string + readonly description: string + readonly markdown: string +} + // From codersdk/templates.go export interface TemplateGroup extends Group { readonly role: TemplateRole From da9fc00a70f94ee9ad97769471e95779ac9ac624 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 7 Dec 2022 16:27:49 +0000 Subject: [PATCH 2/9] add support for example id in route --- coderd/files.go | 6 ++- coderd/templateversions.go | 81 +++++++++++++++++++++++++++++++++----- codersdk/organizations.go | 2 +- examples/examples.go | 3 +- 4 files changed, 80 insertions(+), 12 deletions(-) diff --git a/coderd/files.go b/coderd/files.go index 2c304921f8226..fca345e9c80fd 100644 --- a/coderd/files.go +++ b/coderd/files.go @@ -19,6 +19,10 @@ import ( "github.com/coder/coder/codersdk" ) +const ( + uploadFileContentTypeHeader = "application/x-tar" +) + func (api *API) postFile(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() apiKey := httpmw.APIKey(r) @@ -32,7 +36,7 @@ func (api *API) postFile(rw http.ResponseWriter, r *http.Request) { contentType := r.Header.Get("Content-Type") switch contentType { - case "application/x-tar": + case uploadFileContentTypeHeader: default: httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: fmt.Sprintf("Unsupported content type header %q.", contentType), diff --git a/coderd/templateversions.go b/coderd/templateversions.go index b7c63cf92c1c1..419dc428bb23f 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -2,7 +2,9 @@ package coderd import ( "context" + "crypto/sha256" "database/sql" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -23,6 +25,7 @@ import ( "github.com/coder/coder/coderd/provisionerdserver" "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/codersdk" + "github.com/coder/coder/examples" ) func (api *API) templateVersion(rw http.ResponseWriter, r *http.Request) { @@ -834,19 +837,79 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht // Ensures the "owner" is properly applied. tags := provisionerdserver.MutateTags(apiKey.UserID, req.ProvisionerTags) - file, err := api.Database.GetFileByID(ctx, req.FileID) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ - Message: "File not found.", + if req.ExampleID != "" && req.FileID != uuid.Nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "You cannot specify both an example_id and a file_id.", }) return } - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching file.", - Detail: err.Error(), + + var file database.File + var err error + // if example id is specified we need to copy the embedded tar into a new file in the database + if req.ExampleID != "" { + if !api.Authorize(r, rbac.ActionCreate, rbac.ResourceFile.WithOwner(apiKey.UserID.String())) { + httpapi.Forbidden(rw) + return + } + // ensure we can read the file that either already exists or will be created + if !api.Authorize(r, rbac.ActionRead, rbac.ResourceFile.WithOwner(apiKey.UserID.String())) { + httpapi.Forbidden(rw) + return + } + + // lookup template tar from embedded examples + tar, err := examples.Archive(req.ExampleID) + if err != nil { + if xerrors.Is(err, examples.ErrNotFound) { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Example not found.", + Detail: err.Error(), + }) + return + } + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching example.", + Detail: err.Error(), + }) + return + } + + // upload a copy of the template tar as a file in the database + hashBytes := sha256.Sum256(tar) + hash := hex.EncodeToString(hashBytes[:]) + file, err = api.Database.InsertFile(ctx, database.InsertFileParams{ + ID: uuid.New(), + Hash: hash, + CreatedBy: apiKey.UserID, + CreatedAt: database.Now(), + Mimetype: uploadFileContentTypeHeader, + Data: tar, }) - return + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error creating file.", + Detail: err.Error(), + }) + return + } + } + + if req.FileID != uuid.Nil { + file, err = api.Database.GetFileByID(ctx, req.FileID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ + Message: "File not found.", + }) + return + } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching file.", + Detail: err.Error(), + }) + return + } } if !api.Authorize(r, rbac.ActionRead, file) { diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 2b9f7e906d4a5..2c15e78db1c69 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -39,7 +39,7 @@ type CreateTemplateVersionRequest struct { TemplateID uuid.UUID `json:"template_id,omitempty"` StorageMethod ProvisionerStorageMethod `json:"storage_method" validate:"oneof=file,required"` FileID uuid.UUID `json:"file_id,omitempty" validate:"required_without=ExampleID"` - ExampleID uuid.UUID `json:"example_id,omitempty" validate:"required_without=FileID"` + ExampleID string `json:"example_id,omitempty" validate:"required_without=FileID"` Provisioner ProvisionerType `json:"provisioner" validate:"oneof=terraform echo,required"` ProvisionerTags map[string]string `json:"tags"` diff --git a/examples/examples.go b/examples/examples.go index d34169d8bd489..93894fc863fc8 100644 --- a/examples/examples.go +++ b/examples/examples.go @@ -24,6 +24,7 @@ var ( examples = make([]codersdk.TemplateExample, 0) parseExamples sync.Once archives = singleflight.Group{} + ErrNotFound = xerrors.New("example not found") ) const rootDir = "templates" @@ -116,7 +117,7 @@ func Archive(exampleID string) ([]byte, error) { } if selected.ID == "" { - return nil, xerrors.Errorf("example with id %q not found", exampleID) + return nil, xerrors.Errorf("example with id %q not found: %w", exampleID, ErrNotFound) } exampleFiles, err := fs.Sub(files, path.Join(rootDir, exampleID)) From a9f04f3e4d8e6c4e7cbce68a7da416a2a445f99d Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 7 Dec 2022 16:32:36 +0000 Subject: [PATCH 3/9] move files --- coderd/templates.go | 16 ++++++++++++++++ coderd/users.go | 15 --------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/coderd/templates.go b/coderd/templates.go index 1a97da0dd50a8..a357214eac3b3 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -23,6 +23,7 @@ import ( "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/coderd/telemetry" "github.com/coder/coder/codersdk" + "github.com/coder/coder/examples" ) // Auto-importable templates. These can be auto-imported after the first user @@ -564,6 +565,21 @@ func (api *API) templateDAUs(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, resp) } +func (*API) templateExamples(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + ex, err := examples.List() + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching examples.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, ex) +} + type autoImportTemplateOpts struct { name string archive []byte diff --git a/coderd/users.go b/coderd/users.go index 7bf27ce94c9de..b3e42cba75c91 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -825,21 +825,6 @@ func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, convertUser(updatedUser, organizationIDs)) } -func (_ *API) templateExamples(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - ex, err := examples.List() - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching examples.", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, ex) -} - // updateSiteUserRoles will ensure only site wide roles are passed in as arguments. // If an organization role is included, an error is returned. func (api *API) updateSiteUserRoles(ctx context.Context, args database.UpdateUserRolesParams) (database.User, error) { From 90e6a9d0dde8f118d53b2fa536acd7aca96f9bbc Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 7 Dec 2022 16:39:04 +0000 Subject: [PATCH 4/9] fix existing tests --- coderd/templates.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/coderd/templates.go b/coderd/templates.go index a357214eac3b3..d87247fba8633 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -565,8 +565,16 @@ func (api *API) templateDAUs(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, resp) } -func (*API) templateExamples(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() +func (api *API) templateExamples(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + organization = httpmw.OrganizationParam(r) + ) + + if !api.Authorize(r, rbac.ActionRead, rbac.ResourceTemplate.InOrg(organization.ID)) { + httpapi.ResourceNotFound(rw) + return + } ex, err := examples.List() if err != nil { From 7084cebfff5d1b72ed9b497596427cdcf14c5a9d Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 7 Dec 2022 19:55:41 +0000 Subject: [PATCH 5/9] add tests --- coderd/files.go | 4 +-- coderd/templateversions.go | 2 +- coderd/templateversions_test.go | 46 +++++++++++++++++++++++++++++++++ codersdk/templates.go | 14 ++++++++++ 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/coderd/files.go b/coderd/files.go index fca345e9c80fd..8d01745f919c6 100644 --- a/coderd/files.go +++ b/coderd/files.go @@ -20,7 +20,7 @@ import ( ) const ( - uploadFileContentTypeHeader = "application/x-tar" + tarMimeType = "application/x-tar" ) func (api *API) postFile(rw http.ResponseWriter, r *http.Request) { @@ -36,7 +36,7 @@ func (api *API) postFile(rw http.ResponseWriter, r *http.Request) { contentType := r.Header.Get("Content-Type") switch contentType { - case uploadFileContentTypeHeader: + case tarMimeType: default: httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: fmt.Sprintf("Unsupported content type header %q.", contentType), diff --git a/coderd/templateversions.go b/coderd/templateversions.go index 419dc428bb23f..8222a1229558d 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -883,7 +883,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht Hash: hash, CreatedBy: apiKey.UserID, CreatedAt: database.Now(), - Mimetype: uploadFileContentTypeHeader, + Mimetype: tarMimeType, Data: tar, }) if err != nil { diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index 3c752253f61cb..c4b7ee26cee44 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -15,6 +15,7 @@ import ( "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/provisionerdserver" "github.com/coder/coder/codersdk" + "github.com/coder/coder/examples" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/testutil" @@ -128,6 +129,31 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) { require.Len(t, auditor.AuditLogs, 1) assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs[0].Action) }) + t.Run("Example", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + ls, err := examples.List() + require.NoError(t, err) + + tv, err := client.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{ + Name: "my-example", + StorageMethod: codersdk.ProvisionerStorageMethodFile, + ExampleID: ls[0].ID, + Provisioner: codersdk.ProvisionerTypeEcho, + }) + require.NoError(t, err) + fl, ct, err := client.Download(ctx, tv.Job.FileID) + require.NoError(t, err) + require.Equal(t, "application/x-tar", ct) + tar, err := examples.Archive(ls[0].ID) + require.NoError(t, err) + require.EqualValues(t, tar, fl) + }) } func TestPatchCancelTemplateVersion(t *testing.T) { @@ -997,3 +1023,23 @@ func TestPreviousTemplateVersion(t *testing.T) { require.Equal(t, previousVersion.ID, result.ID) }) } + +func TestTemplateExamples(t *testing.T) { + t.Parallel() + t.Run("OK", 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) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + ex, err := client.TemplateExamples(ctx, user.OrganizationID) + require.NoError(t, err) + ls, err := examples.List() + require.NoError(t, err) + require.EqualValues(t, ls, ex) + }) +} diff --git a/codersdk/templates.go b/codersdk/templates.go index e042a056a6e70..d73570c2da98c 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -246,3 +246,17 @@ type AgentStatsReportResponse struct { // TxBytes is the number of transmitted bytes. TxBytes int64 `json:"tx_bytes"` } + +// TemplateExamples lists example templates embedded in coder. +func (c *Client) TemplateExamples(ctx context.Context, organizationID uuid.UUID) ([]TemplateExample, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/templates/examples", organizationID), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + var templateExamples []TemplateExample + return templateExamples, json.NewDecoder(res.Body).Decode(&templateExamples) +} From 50fb40502f00ce0a8381a1b40c44a3472dd2af46 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 7 Dec 2022 20:11:39 +0000 Subject: [PATCH 6/9] more tests --- coderd/templateversions_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index c4b7ee26cee44..a57e6eade3892 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -140,13 +140,27 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) { ls, err := examples.List() require.NoError(t, err) + // try a bad example ID tv, err := client.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{ + Name: "my-example", + StorageMethod: codersdk.ProvisionerStorageMethodFile, + ExampleID: "not a real ID", + Provisioner: codersdk.ProvisionerTypeEcho, + }) + require.Error(t, err) + require.ErrorContains(t, err, "not found") + + // try a good example ID + tv, err = client.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{ Name: "my-example", StorageMethod: codersdk.ProvisionerStorageMethodFile, ExampleID: ls[0].ID, Provisioner: codersdk.ProvisionerTypeEcho, }) require.NoError(t, err) + require.Equal(t, "my-example", tv.Name) + + // ensure the template tar was uploaded correctly fl, ct, err := client.Download(ctx, tv.Job.FileID) require.NoError(t, err) require.Equal(t, "application/x-tar", ct) From b00a3b92395420ffd7c6c35e75cad21bae88b7cf Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 7 Dec 2022 20:15:57 +0000 Subject: [PATCH 7/9] more tests --- coderd/templateversions_test.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index a57e6eade3892..3b0418a50202a 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -141,7 +141,7 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) { require.NoError(t, err) // try a bad example ID - tv, err := client.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{ + _, err = client.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{ Name: "my-example", StorageMethod: codersdk.ProvisionerStorageMethodFile, ExampleID: "not a real ID", @@ -150,8 +150,20 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) { require.Error(t, err) require.ErrorContains(t, err, "not found") + // try file and example IDs + _, err = client.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{ + Name: "my-example", + StorageMethod: codersdk.ProvisionerStorageMethodFile, + ExampleID: ls[0].ID, + FileID: uuid.New(), + Provisioner: codersdk.ProvisionerTypeEcho, + }) + require.Error(t, err) + require.ErrorContains(t, err, "example_id") + require.ErrorContains(t, err, "file_id") + // try a good example ID - tv, err = client.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{ + tv, err := client.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{ Name: "my-example", StorageMethod: codersdk.ProvisionerStorageMethodFile, ExampleID: ls[0].ID, From 85266d017679b5fcdfd336e8d836dc75cdc0ebf3 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 9 Dec 2022 17:48:33 +0000 Subject: [PATCH 8/9] add icon and tag parsing --- codersdk/templates.go | 12 +++++++----- examples/examples.go | 30 ++++++++++++++++++++++++++++++ examples/examples_test.go | 1 + site/src/api/typesGenerated.ts | 2 ++ 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/codersdk/templates.go b/codersdk/templates.go index d73570c2da98c..21d0870692d1f 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -83,11 +83,13 @@ type UpdateTemplateMeta struct { } type TemplateExample struct { - ID string `json:"id"` - URL string `json:"url"` - Name string `json:"name"` - Description string `json:"description"` - Markdown string `json:"markdown"` + ID string `json:"id"` + URL string `json:"url"` + Name string `json:"name"` + Description string `json:"description"` + Icon string `json:"icon"` + Tags []string `json:"tags"` + Markdown string `json:"markdown"` } // Template returns a single template. diff --git a/examples/examples.go b/examples/examples.go index 93894fc863fc8..78da2079eed6a 100644 --- a/examples/examples.go +++ b/examples/examples.go @@ -87,11 +87,41 @@ func List() ([]codersdk.TemplateExample, error) { return } + tags := []string{} + tagsRaw, exists := frontMatter.FrontMatter["tags"] + if exists { + tagsI, valid := tagsRaw.([]interface{}) + if !valid { + returnError = xerrors.Errorf("example %q tags isn't a slice: type %T", exampleID, tagsRaw) + return + } + for _, tagI := range tagsI { + tag, valid := tagI.(string) + if !valid { + returnError = xerrors.Errorf("example %q tag isn't a string: type %T", exampleID, tagI) + return + } + tags = append(tags, tag) + } + } + + var icon string + iconRaw, exists := frontMatter.FrontMatter["icon"] + if exists { + icon, valid = iconRaw.(string) + if !valid { + returnError = xerrors.Errorf("example %q icon isn't a string", exampleID) + return + } + } + examples = append(examples, codersdk.TemplateExample{ ID: exampleID, URL: exampleURL, Name: name, Description: description, + Icon: icon, + Tags: tags, Markdown: string(frontMatter.Content), }) } diff --git a/examples/examples_test.go b/examples/examples_test.go index 11854cef4347d..551692aee820f 100644 --- a/examples/examples_test.go +++ b/examples/examples_test.go @@ -27,6 +27,7 @@ func TestTemplate(t *testing.T) { assert.NotEmpty(t, eg.Name, "example name should not be empty") assert.NotEmpty(t, eg.Description, "example description should not be empty") assert.NotEmpty(t, eg.Markdown, "example markdown should not be empty") + assert.NotNil(t, eg.Tags, "example tags should not be nil, should be empty array if no tags") _, err := examples.Archive(eg.ID) assert.NoError(t, err, "error archiving example") }) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 345dc6fa7d819..7d790be5c0b64 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -675,6 +675,8 @@ export interface TemplateExample { readonly url: string readonly name: string readonly description: string + readonly icon: string + readonly tags: string[] readonly markdown: string } From 9f3108f3018a4db615970050d2d831cae97ae1bf Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 9 Dec 2022 13:01:10 -0500 Subject: [PATCH 9/9] remove extra test work Co-authored-by: Dean Sheather --- coderd/templateversions_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index 3b0418a50202a..b0858556a2eed 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -1056,8 +1056,6 @@ func TestTemplateExamples(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) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel()