mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: add examples to api (#5331)
Co-authored-by: Dean Sheather <dean@deansheather.com>
This commit is contained in:
@ -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)
|
||||
|
@ -19,6 +19,10 @@ import (
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
const (
|
||||
tarMimeType = "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 tarMimeType:
|
||||
default:
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Unsupported content type header %q.", contentType),
|
||||
|
@ -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,29 @@ func (api *API) templateDAUs(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
|
@ -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: tarMimeType,
|
||||
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) {
|
||||
|
@ -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,57 @@ 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)
|
||||
|
||||
// try a bad example ID
|
||||
_, 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 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{
|
||||
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)
|
||||
tar, err := examples.Archive(ls[0].ID)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, tar, fl)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchCancelTemplateVersion(t *testing.T) {
|
||||
@ -1019,3 +1071,21 @@ func TestPreviousTemplateVersion(t *testing.T) {
|
||||
require.Equal(t, templateBVersion1.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)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user