refactor: templates api logic

This commit is contained in:
2024-11-29 23:52:19 +00:00
parent db03db3b83
commit ae8f62d77d
10 changed files with 561 additions and 428 deletions

View File

@@ -87,7 +87,8 @@ func (p *ReverseProxy) handleRequest(c echo.Context) error {
return echo.NewHTTPError(http.StatusNotFound) return echo.NewHTTPError(http.StatusNotFound)
} }
first := strings.Split(subdomain, ".")[0] ps := strings.Split(subdomain, ".")
first := ps[len(ps)-1]
proxy, ok := p.httpProxies[first] proxy, ok := p.httpProxies[first]
if !ok { if !ok {
return echo.NewHTTPError(http.StatusNotFound) return echo.NewHTTPError(http.StatusNotFound)

View File

@@ -1,113 +0,0 @@
package template
import (
"archive/tar"
"bytes"
"context"
"errors"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/google/uuid"
"github.com/uptrace/bun"
"io"
"time"
)
type createTemplateOptions struct {
name string
description string
}
type templateBuildOptions struct {
imageTag string
buildArgs map[string]*string
}
func createDockerTemplate(ctx context.Context, tx bun.Tx, opts createTemplateOptions) (*template, error) {
id, err := uuid.NewV7()
if err != nil {
return nil, err
}
now := time.Now().Format(time.RFC3339)
t := template{
ID: id,
Name: opts.name,
Description: opts.description,
CreatedOn: now,
LastModifiedOn: now,
IsBuilt: false,
}
dockerfile := templateFile{
TemplateID: id,
FilePath: "Dockerfile",
Content: make([]byte, 0),
}
readme := templateFile{
TemplateID: id,
FilePath: "README.md",
Content: make([]byte, 0),
}
files := []*templateFile{&dockerfile, &readme}
if err = tx.NewInsert().Model(&t).Returning("*").Scan(ctx); err != nil {
return nil, err
}
if err = tx.NewInsert().Model(&files).Scan(ctx); err != nil {
return nil, err
}
t.Files = files
return &t, nil
}
func buildDockerTemplate(ctx context.Context, docker *client.Client, tmpl *template, opts templateBuildOptions) (io.ReadCloser, error) {
if len(tmpl.Files) == 0 {
return nil, errors.New("cannot build docker template: no files in template")
}
buf := new(bytes.Buffer)
tw := tar.NewWriter(buf)
defer tw.Close()
var dockerfile []byte
for _, file := range tmpl.Files {
if file.FilePath == "Dockerfile" {
dockerfile = file.Content
break
}
}
if len(dockerfile) == 0 {
return nil, errors.New("cannot build docker template: template does not contain Dockerfile")
}
h := tar.Header{
Name: "Dockerfile",
Size: int64(len(dockerfile)),
}
err := tw.WriteHeader(&h)
if err != nil {
return nil, err
}
_, err = tw.Write(dockerfile)
if err != nil {
return nil, err
}
r := bytes.NewReader(buf.Bytes())
res, err := docker.ImageBuild(ctx, r, types.ImageBuildOptions{
Context: r,
Tags: []string{opts.imageTag},
BuildArgs: opts.buildArgs,
})
if err != nil {
return nil, err
}
return res.Body, nil
}

View File

@@ -1,15 +1,12 @@
package template package template
import ( import (
"bufio"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"io" "io"
"net/http" "net/http"
"strings"
"tesseract/internal/service" "tesseract/internal/service"
) )
@@ -19,7 +16,7 @@ type createTemplateRequestBody struct {
Documentation string `json:"documentation"` Documentation string `json:"documentation"`
} }
type updateTemplateRequestBody struct { type postTemplateRequestBody struct {
Description *string `json:"description"` Description *string `json:"description"`
Files []templateFile `json:"files"` Files []templateFile `json:"files"`
@@ -27,92 +24,52 @@ type updateTemplateRequestBody struct {
BuildArgs map[string]*string `json:"buildArgs"` BuildArgs map[string]*string `json:"buildArgs"`
} }
type templateBuildLogEvent struct {
Type string `json:"type"`
LogContent string `json:"logContent"`
}
type templateBuildFinishedEvent struct {
Type string `json:"type"`
Template *template `json:"template"`
}
func fetchAllTemplates(c echo.Context) error { func fetchAllTemplates(c echo.Context) error {
db := service.Database(c) mgr := templateManagerFrom(c)
templates, err := mgr.findAllTemplates(c.Request().Context())
var templates []template
err := db.NewSelect().Model(&templates).Scan(c.Request().Context())
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return c.JSON(http.StatusOK, make([]template, 0))
}
return err return err
} }
if len(templates) == 0 {
return c.JSON(http.StatusOK, make([]template, 0))
}
return c.JSON(http.StatusOK, templates) return c.JSON(http.StatusOK, templates)
} }
func fetchTemplate(c echo.Context) error { func fetchTemplate(c echo.Context) error {
db := service.Database(c) mgr := templateManagerFrom(c)
template, err := mgr.findTemplate(c.Request().Context(), c.Param("templateName"))
name := c.Param("templateName")
if strings.TrimSpace(name) == "" {
return echo.NewHTTPError(http.StatusNotFound)
}
var tmpl template
err := db.NewSelect().Model(&tmpl).
Relation("Files").
Where("name = ?", name).
Scan(c.Request().Context())
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, errTemplateNotFound) {
return echo.NewHTTPError(http.StatusNotFound) return echo.NewHTTPError(http.StatusNotFound)
} }
return err return err
} }
return c.JSON(http.StatusOK, template)
if len(tmpl.Files) > 0 {
tmpl.FileMap = make(map[string]*templateFile)
}
for _, f := range tmpl.Files {
tmpl.FileMap[f.FilePath] = f
}
return c.JSON(http.StatusOK, tmpl)
} }
func createOrUpdateTemplate(c echo.Context) error { func createOrUpdateTemplate(c echo.Context) error {
db := service.Database(c) mgr := templateManagerFrom(c)
exists, err := mgr.hasTemplate(c.Request().Context(), c.Param("templateName"))
name := c.Param("templateName")
if strings.TrimSpace(name) == "" {
return echo.NewHTTPError(http.StatusNotFound)
}
exists, err := db.NewSelect().
Table("templates").
Where("name = ?", name).
Exists(c.Request().Context())
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return createTemplate(c)
}
return err return err
} }
if !exists { if !exists {
return createTemplate(c) return createTemplate(c)
} }
return updateTemplate(c) var body postTemplateRequestBody
err = json.NewDecoder(c.Request().Body).Decode(&body)
if err != nil {
return err
}
if body.ImageTag != nil || body.BuildArgs != nil {
return buildTemplate(c, body)
}
return updateTemplate(c, body)
} }
func createTemplate(c echo.Context) error { func createTemplate(c echo.Context) error {
db := service.Database(c) mgr := templateManagerFrom(c)
name := c.Param("templateName") name := c.Param("templateName")
var body createTemplateRequestBody var body createTemplateRequestBody
@@ -121,86 +78,49 @@ func createTemplate(c echo.Context) error {
return err return err
} }
tx, err := db.BeginTx(c.Request().Context(), nil) createdTemplate, err := mgr.createTemplate(c.Request().Context(), createTemplateOptions{
if err != nil {
return err
}
createdTemplate, err := createDockerTemplate(c.Request().Context(), tx, createTemplateOptions{
name: name, name: name,
description: body.Description, description: body.Description,
}) })
if err != nil { if err != nil {
_ = tx.Rollback()
return err
}
if err = tx.Commit(); err != nil {
_ = tx.Rollback()
return err return err
} }
return c.JSON(http.StatusOK, createdTemplate) return c.JSON(http.StatusOK, createdTemplate)
} }
func updateTemplate(c echo.Context) error { func updateTemplate(c echo.Context, body postTemplateRequestBody) error {
db := service.Database(c)
name := c.Param("templateName") name := c.Param("templateName")
mgr := templateManagerFrom(c)
ctx := c.Request().Context()
var body updateTemplateRequestBody updatedTemplate, err := mgr.updateTemplate(ctx, name, updateTemplateOptions{
err := json.NewDecoder(c.Request().Body).Decode(&body) description: *body.Description,
})
if err != nil { if err != nil {
return err if errors.Is(err, errTemplateNotFound) {
}
if body.BuildArgs != nil && body.ImageTag == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Image tag must be specified if buildArgs is passed")
}
tx, err := db.BeginTx(c.Request().Context(), nil)
if err != nil {
return err
}
var tmpl template
err = tx.NewSelect().Model(&tmpl).
Where("name = ?", name).
Scan(c.Request().Context())
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return echo.NewHTTPError(http.StatusNotFound) return echo.NewHTTPError(http.StatusNotFound)
} }
return err return err
} }
if body.Description != nil { return c.JSON(http.StatusOK, &updatedTemplate)
tmpl.Description = *body.Description }
_, err = tx.NewUpdate().Model(&tmpl).
Column("description"). func buildTemplate(c echo.Context, body postTemplateRequestBody) error {
WherePK(). mgr := templateManagerFrom(c)
Exec(c.Request().Context()) name := c.Param("templateName")
ctx := c.Request().Context()
template, err := mgr.findTemplate(ctx, name)
if err != nil { if err != nil {
_ = tx.Rollback() if errors.Is(err, errTemplateNotFound) {
return echo.NewHTTPError(http.StatusNotFound)
}
return err return err
} }
if err = tx.Commit(); err != nil { outputChan, err := mgr.buildTemplate(ctx, template, buildTemplateOptions{
_ = tx.Rollback()
return err
}
}
if body.ImageTag != nil {
err = tx.NewSelect().Model(&tmpl.Files).
Where("template_id = ?", tmpl.ID).
Scan(c.Request().Context())
if err != nil {
_ = tx.Rollback()
return err
}
docker := service.DockerClient(c)
log, err := buildDockerTemplate(c.Request().Context(), docker, &tmpl, templateBuildOptions{
imageTag: *body.ImageTag, imageTag: *body.ImageTag,
buildArgs: body.BuildArgs, buildArgs: body.BuildArgs,
}) })
@@ -213,116 +133,30 @@ func updateTemplate(c echo.Context) error {
w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive") w.Header().Set("Connection", "keep-alive")
scanner := bufio.NewScanner(log) for o := range outputChan {
switch o := o.(type) {
var imageID string case error:
for scanner.Scan() {
t := scanner.Text()
fmt.Println("DOCKER LOG: ", t)
var msg map[string]any
err = json.Unmarshal([]byte(t), &msg)
if err != nil {
return err return err
} case string:
if _, err = w.Write([]byte(o)); err != nil {
if stream, ok := msg["stream"].(string); ok {
if _, err = w.Write([]byte(stream)); err != nil {
return err return err
} }
w.Flush() w.Flush()
} else if errmsg, ok := msg["error"].(string); ok {
if _, err = w.Write([]byte(errmsg + "\n")); err != nil {
return err
} }
w.Flush()
} else if status, ok := msg["status"].(string); ok {
var text string
if progress, ok := msg["progress"].(string); ok {
text = fmt.Sprintf("%v: %v\n", status, progress)
} else {
text = status + "\n"
}
if _, err = w.Write([]byte(text)); err != nil {
return err
}
w.Flush()
} else if aux, ok := msg["aux"].(map[string]any); ok {
if id, ok := aux["ID"].(string); ok {
imageID = id
}
}
}
if imageID != "" {
img := TemplateImage{
TemplateID: tmpl.ID,
ImageTag: *body.ImageTag,
ImageID: imageID,
}
_, err = tx.NewInsert().Model(&img).Exec(c.Request().Context())
if err != nil {
_ = tx.Rollback()
return err
}
}
if err = tx.Commit(); err != nil {
_ = tx.Rollback()
return err
} }
return nil return nil
} }
if err = tx.Commit(); err != nil {
_ = tx.Rollback()
return err
}
return c.JSON(http.StatusOK, &tmpl)
}
func deleteTemplate(c echo.Context) error { func deleteTemplate(c echo.Context) error {
templateName := c.Param("templateName") mgr := templateManagerFrom(c)
if templateName == "" { name := c.Param("templateName")
err := mgr.deleteTemplate(c.Request().Context(), name)
if err != nil {
if errors.Is(err, errTemplateNotFound) {
return echo.NewHTTPError(http.StatusNotFound) return echo.NewHTTPError(http.StatusNotFound)
} }
db := service.Database(c)
tx, err := db.BeginTx(c.Request().Context(), nil)
if err != nil {
return err
}
res, err := tx.NewDelete().Table("templates").
Where("name = ?", templateName).
Exec(c.Request().Context())
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return echo.NewHTTPError(http.StatusNotFound)
}
_ = tx.Rollback()
return err
}
count, err := res.RowsAffected()
if err != nil {
_ = tx.Rollback()
return err
}
if count != 1 {
_ = tx.Rollback()
return echo.NewHTTPError(http.StatusInternalServerError)
}
if err = tx.Commit(); err != nil {
_ = tx.Rollback()
return err return err
} }
@@ -330,37 +164,13 @@ func deleteTemplate(c echo.Context) error {
} }
func fetchTemplateFile(c echo.Context) error { func fetchTemplateFile(c echo.Context) error {
mgr := templateManagerFrom(c)
templateName := c.Param("templateName") templateName := c.Param("templateName")
if templateName == "" {
return echo.NewHTTPError(http.StatusNotFound)
}
filePath := c.Param("filePath") filePath := c.Param("filePath")
if filePath == "" {
return echo.NewHTTPError(http.StatusNotFound)
}
db := service.Database(c) file, err := mgr.findTemplateFile(c.Request().Context(), templateName, filePath)
var tmpl template
err := db.NewSelect().Model(&tmpl).
Column("id").
Where("name = ?", templateName).
Scan(c.Request().Context())
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, errTemplateNotFound) || errors.Is(err, errTemplateFileNotFound) {
return echo.NewHTTPError(http.StatusNotFound)
}
return err
}
var file templateFile
err = db.NewSelect().Model(&file).
Where("template_id = ?", tmpl.ID).
Where("file_path = ?", filePath).
Scan(c.Request().Context())
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return echo.NewHTTPError(http.StatusNotFound) return echo.NewHTTPError(http.StatusNotFound)
} }
return err return err
@@ -370,74 +180,40 @@ func fetchTemplateFile(c echo.Context) error {
} }
func updateTemplateFile(c echo.Context) error { func updateTemplateFile(c echo.Context) error {
mgr := templateManagerFrom(c)
templateName := c.Param("templateName") templateName := c.Param("templateName")
if templateName == "" {
return echo.NewHTTPError(http.StatusNotFound)
}
filePath := c.Param("filePath") filePath := c.Param("filePath")
if filePath == "" {
return echo.NewHTTPError(http.StatusNotFound)
}
db := service.Database(c)
tx, err := db.BeginTx(c.Request().Context(), nil)
if err != nil {
return err
}
var tmpl template
err = tx.NewSelect().Model(&tmpl).
Column("id").
Where("name = ?", templateName).
Scan(c.Request().Context())
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return echo.NewHTTPError(http.StatusNotFound)
}
return err
}
newContent, err := io.ReadAll(c.Request().Body) newContent, err := io.ReadAll(c.Request().Body)
if err != nil { if err != nil {
return err return err
} }
_, err = tx.NewUpdate().Table("template_files"). err = mgr.updateTemplateFile(c.Request().Context(), templateName, filePath, newContent)
Set("content = ?", newContent).
Where("template_id = ?", tmpl.ID).
Where("file_path = ?", filePath).
Exec(c.Request().Context())
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, errTemplateNotFound) || errors.Is(err, errTemplateFileNotFound) {
return echo.NewHTTPError(http.StatusNotFound) return echo.NewHTTPError(http.StatusNotFound)
} }
return err return err
} }
if err = tx.Commit(); err != nil {
_ = tx.Rollback()
return err
}
return c.NoContent(http.StatusOK) return c.NoContent(http.StatusOK)
} }
func fetchAllTemplateImages(c echo.Context) error { func fetchAllTemplateImages(c echo.Context) error {
db := service.Database(c) db := service.Database(c)
var images []TemplateImage var images []Image
err := db.NewSelect().Model(&images).Scan(c.Request().Context()) err := db.NewSelect().Model(&images).Scan(c.Request().Context())
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return c.JSON(http.StatusOK, make([]TemplateImage, 0)) return c.JSON(http.StatusOK, make([]Image, 0))
} }
return err return err
} }
if len(images) == 0 { if len(images) == 0 {
return c.JSON(http.StatusOK, make([]TemplateImage, 0)) return c.JSON(http.StatusOK, make([]Image, 0))
} }
return c.JSON(http.StatusOK, images) return c.JSON(http.StatusOK, images)

View File

@@ -0,0 +1,45 @@
package template
import (
"net/http"
"tesseract/internal/service"
"github.com/labstack/echo/v4"
)
func newTemplateManagerMiddleware(service service.Services) echo.MiddlewareFunc {
mgr := templateManager{
db: service.Database,
dockerClient: service.DockerClient,
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Set("templateManager", &mgr)
return next(c)
}
}
}
func validateTemplateName(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
templateName := c.Param("templateName")
if templateName == "" || !templateNameRegex.MatchString(templateName) {
return echo.NewHTTPError(http.StatusNotFound)
}
return next(c)
}
}
func validateTemplateFilePath(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
filePath := c.Param("filePath")
if filePath == "" {
return echo.NewHTTPError(http.StatusNotFound)
}
return next(c)
}
}
func templateManagerFrom(c echo.Context) *templateManager {
return c.Get("templateManager").(*templateManager)
}

View File

@@ -2,14 +2,16 @@ package template
import ( import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"tesseract/internal/service"
) )
func DefineRoutes(g *echo.Group) { func DefineRoutes(g *echo.Group, services service.Services) {
g.Use(newTemplateManagerMiddleware(services))
g.GET("/templates", fetchAllTemplates) g.GET("/templates", fetchAllTemplates)
g.GET("/templates/:templateName", fetchTemplate) g.GET("/templates/:templateName", fetchTemplate, validateTemplateName)
g.POST("/templates/:templateName", createOrUpdateTemplate) g.POST("/templates/:templateName", createOrUpdateTemplate, validateTemplateName)
g.DELETE("/templates/:templateName", deleteTemplate) g.DELETE("/templates/:templateName", deleteTemplate, validateTemplateName)
g.GET("/templates/:templateName/:filePath", fetchTemplateFile) g.GET("/templates/:templateName/:filePath", fetchTemplateFile, validateTemplateName, validateTemplateFilePath)
g.POST("/templates/:templateName/:filePath", updateTemplateFile) g.POST("/templates/:templateName/:filePath", updateTemplateFile, validateTemplateName, validateTemplateFilePath)
g.GET("/template-images", fetchAllTemplateImages) g.GET("/template-images", fetchAllTemplateImages)
} }

View File

@@ -3,8 +3,12 @@ package template
import ( import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/uptrace/bun" "github.com/uptrace/bun"
"regexp"
) )
// templateNameRegex is a regex to test whether a given template name is valid
var templateNameRegex = regexp.MustCompile("^[\\w-]+$")
type template struct { type template struct {
bun.BaseModel `bun:"table:templates,alias:template"` bun.BaseModel `bun:"table:templates,alias:template"`
@@ -27,7 +31,7 @@ type templateFile struct {
Content []byte `bun:"type:blob" json:"content"` Content []byte `bun:"type:blob" json:"content"`
} }
type TemplateImage struct { type Image struct {
bun.BaseModel `bun:"table:template_images,alias:template_images"` bun.BaseModel `bun:"table:template_images,alias:template_images"`
TemplateID uuid.UUID `bun:"type:uuid" json:"-"` TemplateID uuid.UUID `bun:"type:uuid" json:"-"`

View File

@@ -0,0 +1,417 @@
package template
import (
"archive/tar"
"bufio"
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/google/uuid"
"github.com/uptrace/bun"
"time"
)
type templateManager struct {
db *bun.DB
dockerClient *client.Client
}
type createTemplateOptions struct {
name string
description string
}
type updateTemplateOptions struct {
tx *bun.Tx
description string
}
type buildTemplateOptions struct {
tx *bun.Tx
imageTag string
buildArgs map[string]*string
}
var errTemplateNotFound = errors.New("template not found")
var errTemplateFileNotFound = errors.New("template file not found")
func (mgr *templateManager) beginTx(ctx context.Context) (bun.Tx, error) {
tx, err := mgr.db.BeginTx(ctx, nil)
if err != nil {
return bun.Tx{}, err
}
return tx, nil
}
func (mgr *templateManager) findAllTemplates(ctx context.Context) ([]template, error) {
var templates []template
err := mgr.db.NewSelect().Model(&templates).Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return make([]template, 0), nil
}
return nil, err
}
if len(templates) == 0 {
return make([]template, 0), nil
}
return templates, nil
}
func (mgr *templateManager) findTemplate(ctx context.Context, name string) (*template, error) {
var template template
err := mgr.db.NewSelect().Model(&template).
Relation("Files").
Where("name = ?", name).
Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errTemplateNotFound
}
return nil, err
}
if len(template.Files) > 0 {
template.FileMap = make(map[string]*templateFile)
}
for _, f := range template.Files {
template.FileMap[f.FilePath] = f
}
return &template, nil
}
func (mgr *templateManager) hasTemplate(ctx context.Context, name string) (bool, error) {
exists, err := mgr.db.NewSelect().
Table("templates").
Where("name = ?", name).
Exists(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return false, err
}
return exists, nil
}
func (mgr *templateManager) createTemplate(ctx context.Context, opts createTemplateOptions) (*template, error) {
tx, err := mgr.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
id, err := uuid.NewV7()
if err != nil {
return nil, err
}
now := time.Now().Format(time.RFC3339)
t := template{
ID: id,
Name: opts.name,
Description: opts.description,
CreatedOn: now,
LastModifiedOn: now,
IsBuilt: false,
}
dockerfile := templateFile{
TemplateID: id,
FilePath: "Dockerfile",
Content: make([]byte, 0),
}
readme := templateFile{
TemplateID: id,
FilePath: "README.md",
Content: make([]byte, 0),
}
files := []*templateFile{&dockerfile, &readme}
if err = tx.NewInsert().Model(&t).Returning("*").Scan(ctx); err != nil {
return nil, err
}
if err = tx.NewInsert().Model(&files).Scan(ctx); err != nil {
return nil, err
}
t.Files = files
if err = tx.Commit(); err != nil {
_ = tx.Rollback()
return nil, err
}
return &t, nil
}
func (mgr *templateManager) updateTemplate(ctx context.Context, name string, opts updateTemplateOptions) (*template, error) {
tx := opts.tx
autoCommit := false
if tx == nil {
_tx, err := mgr.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
autoCommit = true
tx = &_tx
}
var template template
err := tx.NewUpdate().Model(&template).
Where("name = ?", name).
Set("description = ?", opts.description).
Returning("*").
Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errTemplateNotFound
}
return nil, err
}
if autoCommit {
if err = tx.Commit(); err != nil {
_ = tx.Rollback()
return nil, err
}
}
return &template, nil
}
func (mgr *templateManager) buildTemplate(ctx context.Context, template *template, opts buildTemplateOptions) (<-chan any, error) {
tx := opts.tx
autoCommit := false
if tx == nil {
_tx, err := mgr.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
autoCommit = true
tx = &_tx
}
if len(template.Files) == 0 {
return nil, errors.New("cannot build docker template: no files in template")
}
buf := new(bytes.Buffer)
tw := tar.NewWriter(buf)
defer tw.Close()
var dockerfile []byte
for _, file := range template.Files {
if file.FilePath == "Dockerfile" {
dockerfile = file.Content
break
}
}
if len(dockerfile) == 0 {
return nil, errors.New("cannot build docker template: template does not contain Dockerfile")
}
h := tar.Header{
Name: "Dockerfile",
Size: int64(len(dockerfile)),
}
err := tw.WriteHeader(&h)
if err != nil {
return nil, err
}
_, err = tw.Write(dockerfile)
if err != nil {
return nil, err
}
r := bytes.NewReader(buf.Bytes())
res, err := mgr.dockerClient.ImageBuild(ctx, r, types.ImageBuildOptions{
Context: r,
Tags: []string{opts.imageTag},
BuildArgs: opts.buildArgs,
})
if err != nil {
return nil, err
}
outputChan := make(chan any)
go func() {
scanner := bufio.NewScanner(res.Body)
var imageID string
for scanner.Scan() {
t := scanner.Text()
fmt.Println("DOCKER LOG: ", t)
var msg map[string]any
err = json.Unmarshal([]byte(t), &msg)
if err != nil {
outputChan <- err
}
if stream, ok := msg["stream"].(string); ok {
outputChan <- stream
} else if errmsg, ok := msg["error"].(string); ok {
outputChan <- errmsg + "\n"
} else if status, ok := msg["status"].(string); ok {
var text string
if progress, ok := msg["progress"].(string); ok {
text = fmt.Sprintf("%v: %v\n", status, progress)
} else {
text = status + "\n"
}
outputChan <- text
} else if aux, ok := msg["aux"].(map[string]any); ok {
if id, ok := aux["ID"].(string); ok {
imageID = id
}
}
}
var img *Image
if imageID != "" {
img = &Image{
TemplateID: template.ID,
ImageTag: opts.imageTag,
ImageID: imageID,
}
_, err = tx.NewInsert().Model(img).Exec(ctx)
if err != nil {
_ = tx.Rollback()
outputChan <- err
}
}
if autoCommit {
if err = tx.Commit(); err != nil {
_ = tx.Rollback()
outputChan <- err
}
}
if img != nil {
outputChan <- img
}
close(outputChan)
}()
return outputChan, nil
}
func (mgr *templateManager) deleteTemplate(ctx context.Context, name string) error {
tx, err := mgr.db.BeginTx(ctx, nil)
if err != nil {
return err
}
res, err := tx.NewDelete().Table("templates").
Where("name = ?", name).
Exec(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return errTemplateNotFound
}
_ = tx.Rollback()
return err
}
count, err := res.RowsAffected()
if err != nil {
_ = tx.Rollback()
return err
}
if count != 1 {
_ = tx.Rollback()
return errors.New("unexpected number of templates deleted")
}
if err = tx.Commit(); err != nil {
_ = tx.Rollback()
return err
}
return nil
}
func (mgr *templateManager) findTemplateFile(ctx context.Context, templateName, filePath string) (*templateFile, error) {
var tmpl template
err := mgr.db.NewSelect().Model(&tmpl).
Column("id").
Where("name = ?", templateName).
Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errTemplateNotFound
}
return nil, err
}
var file templateFile
err = mgr.db.NewSelect().Model(&file).
Where("template_id = ?", tmpl.ID).
Where("file_path = ?", filePath).
Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errTemplateFileNotFound
}
return nil, err
}
return &file, nil
}
func (mgr *templateManager) updateTemplateFile(ctx context.Context, templateName, filePath string, content []byte) error {
tx, err := mgr.db.BeginTx(ctx, nil)
if err != nil {
return err
}
var template template
err = tx.NewSelect().Model(&template).
Column("id").
Where("name = ?", templateName).
Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return errTemplateNotFound
}
return err
}
_, err = tx.NewUpdate().Table("template_files").
Set("content = ?", content).
Where("template_id = ?", template.ID).
Where("file_path = ?", filePath).
Exec(ctx)
if err != nil {
_ = tx.Rollback()
if errors.Is(err, sql.ErrNoRows) {
return errTemplateFileNotFound
}
return err
}
if err = tx.Commit(); err != nil {
_ = tx.Rollback()
return err
}
return nil
}

View File

@@ -1,8 +1,9 @@
package workspace package workspace
import ( import (
"github.com/labstack/echo/v4"
"tesseract/internal/service" "tesseract/internal/service"
"github.com/labstack/echo/v4"
) )
func newWorkspaceManagerMiddleware(services service.Services) echo.MiddlewareFunc { func newWorkspaceManagerMiddleware(services service.Services) echo.MiddlewareFunc {

View File

@@ -131,7 +131,7 @@ func (mgr workspaceManager) createWorkspace(ctx context.Context, opts createWork
return nil, err return nil, err
} }
var img template.TemplateImage var img template.Image
err = tx.NewSelect().Model(&img). err = tx.NewSelect().Model(&img).
Where("image_id = ?", opts.imageID). Where("image_id = ?", opts.imageID).
Scan(ctx) Scan(ctx)

View File

@@ -80,7 +80,7 @@ func main() {
g := apiServer.Group("/api") g := apiServer.Group("/api")
workspace.DefineRoutes(g, services) workspace.DefineRoutes(g, services)
template.DefineRoutes(g) template.DefineRoutes(g, services)
apiServer.HTTPErrorHandler = func(err error, c echo.Context) { apiServer.HTTPErrorHandler = func(err error, c echo.Context) {
var he *echo.HTTPError var he *echo.HTTPError