From ae8f62d77d323c83519e071ae8064a4d391bbbdf Mon Sep 17 00:00:00 2001 From: Kenneth Date: Fri, 29 Nov 2024 23:52:19 +0000 Subject: [PATCH] refactor: templates api logic --- internal/reverseproxy/proxy.go | 3 +- internal/template/docker_template.go | 113 ------- internal/template/http_handlers.go | 384 +++++----------------- internal/template/middleware.go | 45 +++ internal/template/routes.go | 14 +- internal/template/template.go | 6 +- internal/template/template_manager.go | 417 ++++++++++++++++++++++++ internal/workspace/middleware.go | 3 +- internal/workspace/workspace_manager.go | 2 +- main.go | 2 +- 10 files changed, 561 insertions(+), 428 deletions(-) delete mode 100644 internal/template/docker_template.go create mode 100644 internal/template/middleware.go create mode 100644 internal/template/template_manager.go diff --git a/internal/reverseproxy/proxy.go b/internal/reverseproxy/proxy.go index c0c6f62..ba49c6f 100644 --- a/internal/reverseproxy/proxy.go +++ b/internal/reverseproxy/proxy.go @@ -87,7 +87,8 @@ func (p *ReverseProxy) handleRequest(c echo.Context) error { return echo.NewHTTPError(http.StatusNotFound) } - first := strings.Split(subdomain, ".")[0] + ps := strings.Split(subdomain, ".") + first := ps[len(ps)-1] proxy, ok := p.httpProxies[first] if !ok { return echo.NewHTTPError(http.StatusNotFound) diff --git a/internal/template/docker_template.go b/internal/template/docker_template.go deleted file mode 100644 index cbeb1fa..0000000 --- a/internal/template/docker_template.go +++ /dev/null @@ -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 -} diff --git a/internal/template/http_handlers.go b/internal/template/http_handlers.go index 686abe4..11e7298 100644 --- a/internal/template/http_handlers.go +++ b/internal/template/http_handlers.go @@ -1,15 +1,12 @@ package template import ( - "bufio" "database/sql" "encoding/json" "errors" - "fmt" "github.com/labstack/echo/v4" "io" "net/http" - "strings" "tesseract/internal/service" ) @@ -19,7 +16,7 @@ type createTemplateRequestBody struct { Documentation string `json:"documentation"` } -type updateTemplateRequestBody struct { +type postTemplateRequestBody struct { Description *string `json:"description"` Files []templateFile `json:"files"` @@ -27,92 +24,52 @@ type updateTemplateRequestBody struct { 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 { - db := service.Database(c) - - var templates []template - err := db.NewSelect().Model(&templates).Scan(c.Request().Context()) + mgr := templateManagerFrom(c) + templates, err := mgr.findAllTemplates(c.Request().Context()) if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return c.JSON(http.StatusOK, make([]template, 0)) - } return err } - - if len(templates) == 0 { - return c.JSON(http.StatusOK, make([]template, 0)) - } return c.JSON(http.StatusOK, templates) } func fetchTemplate(c echo.Context) error { - db := service.Database(c) - - 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()) + mgr := templateManagerFrom(c) + template, err := mgr.findTemplate(c.Request().Context(), c.Param("templateName")) if err != nil { - if errors.Is(err, sql.ErrNoRows) { + if errors.Is(err, errTemplateNotFound) { return echo.NewHTTPError(http.StatusNotFound) } return err } - - 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) + return c.JSON(http.StatusOK, template) } func createOrUpdateTemplate(c echo.Context) error { - db := service.Database(c) - - 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()) + mgr := templateManagerFrom(c) + exists, err := mgr.hasTemplate(c.Request().Context(), c.Param("templateName")) if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return createTemplate(c) - } return err } - if !exists { 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 { - db := service.Database(c) + mgr := templateManagerFrom(c) name := c.Param("templateName") var body createTemplateRequestBody @@ -121,208 +78,85 @@ func createTemplate(c echo.Context) error { return err } - tx, err := db.BeginTx(c.Request().Context(), nil) - if err != nil { - return err - } - - createdTemplate, err := createDockerTemplate(c.Request().Context(), tx, createTemplateOptions{ + createdTemplate, err := mgr.createTemplate(c.Request().Context(), createTemplateOptions{ name: name, description: body.Description, }) if err != nil { - _ = tx.Rollback() - return err - } - - if err = tx.Commit(); err != nil { - _ = tx.Rollback() return err } return c.JSON(http.StatusOK, createdTemplate) } -func updateTemplate(c echo.Context) error { - db := service.Database(c) +func updateTemplate(c echo.Context, body postTemplateRequestBody) error { name := c.Param("templateName") + mgr := templateManagerFrom(c) + ctx := c.Request().Context() - var body updateTemplateRequestBody - err := json.NewDecoder(c.Request().Body).Decode(&body) + updatedTemplate, err := mgr.updateTemplate(ctx, name, updateTemplateOptions{ + description: *body.Description, + }) if err != nil { - return err - } - - 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) { + if errors.Is(err, errTemplateNotFound) { return echo.NewHTTPError(http.StatusNotFound) } return err } - if body.Description != nil { - tmpl.Description = *body.Description - _, err = tx.NewUpdate().Model(&tmpl). - Column("description"). - WherePK(). - Exec(c.Request().Context()) - if err != nil { - _ = tx.Rollback() - return err + return c.JSON(http.StatusOK, &updatedTemplate) +} + +func buildTemplate(c echo.Context, body postTemplateRequestBody) error { + mgr := templateManagerFrom(c) + name := c.Param("templateName") + ctx := c.Request().Context() + + template, err := mgr.findTemplate(ctx, name) + if err != nil { + if errors.Is(err, errTemplateNotFound) { + return echo.NewHTTPError(http.StatusNotFound) } - - if err = tx.Commit(); err != nil { - _ = 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, - buildArgs: body.BuildArgs, - }) - if err != nil { - return err - } - - w := c.Response() - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - - scanner := bufio.NewScanner(log) - - 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 { - return err - } - - if stream, ok := msg["stream"].(string); ok { - if _, err = w.Write([]byte(stream)); err != nil { - return err - } - 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 - } - - if err = tx.Commit(); err != nil { - _ = tx.Rollback() return err } - return c.JSON(http.StatusOK, &tmpl) + outputChan, err := mgr.buildTemplate(ctx, template, buildTemplateOptions{ + imageTag: *body.ImageTag, + buildArgs: body.BuildArgs, + }) + if err != nil { + return err + } + + w := c.Response() + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + for o := range outputChan { + switch o := o.(type) { + case error: + return err + case string: + if _, err = w.Write([]byte(o)); err != nil { + return err + } + w.Flush() + } + } + + return nil } func deleteTemplate(c echo.Context) error { - templateName := c.Param("templateName") - if templateName == "" { - return echo.NewHTTPError(http.StatusNotFound) - } + mgr := templateManagerFrom(c) + name := c.Param("templateName") - db := service.Database(c) - - tx, err := db.BeginTx(c.Request().Context(), nil) + err := mgr.deleteTemplate(c.Request().Context(), name) 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) { + if errors.Is(err, errTemplateNotFound) { 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 } @@ -330,37 +164,13 @@ func deleteTemplate(c echo.Context) error { } func fetchTemplateFile(c echo.Context) error { + mgr := templateManagerFrom(c) templateName := c.Param("templateName") - if templateName == "" { - return echo.NewHTTPError(http.StatusNotFound) - } - filePath := c.Param("filePath") - if filePath == "" { - return echo.NewHTTPError(http.StatusNotFound) - } - db := service.Database(c) - - var tmpl template - err := db.NewSelect().Model(&tmpl). - Column("id"). - Where("name = ?", templateName). - Scan(c.Request().Context()) + file, err := mgr.findTemplateFile(c.Request().Context(), templateName, filePath) if err != nil { - if errors.Is(err, sql.ErrNoRows) { - 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) { + if errors.Is(err, errTemplateNotFound) || errors.Is(err, errTemplateFileNotFound) { return echo.NewHTTPError(http.StatusNotFound) } return err @@ -370,74 +180,40 @@ func fetchTemplateFile(c echo.Context) error { } func updateTemplateFile(c echo.Context) error { + mgr := templateManagerFrom(c) templateName := c.Param("templateName") - if templateName == "" { - return echo.NewHTTPError(http.StatusNotFound) - } - 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) if err != nil { return err } - _, err = tx.NewUpdate().Table("template_files"). - Set("content = ?", newContent). - Where("template_id = ?", tmpl.ID). - Where("file_path = ?", filePath). - Exec(c.Request().Context()) + err = mgr.updateTemplateFile(c.Request().Context(), templateName, filePath, newContent) 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 } - if err = tx.Commit(); err != nil { - _ = tx.Rollback() - return err - } - return c.NoContent(http.StatusOK) } func fetchAllTemplateImages(c echo.Context) error { db := service.Database(c) - var images []TemplateImage + var images []Image err := db.NewSelect().Model(&images).Scan(c.Request().Context()) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return c.JSON(http.StatusOK, make([]TemplateImage, 0)) + return c.JSON(http.StatusOK, make([]Image, 0)) } return err } 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) diff --git a/internal/template/middleware.go b/internal/template/middleware.go new file mode 100644 index 0000000..00e1ddf --- /dev/null +++ b/internal/template/middleware.go @@ -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) +} diff --git a/internal/template/routes.go b/internal/template/routes.go index ff23751..6398b1a 100644 --- a/internal/template/routes.go +++ b/internal/template/routes.go @@ -2,14 +2,16 @@ package template import ( "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/:templateName", fetchTemplate) - g.POST("/templates/:templateName", createOrUpdateTemplate) - g.DELETE("/templates/:templateName", deleteTemplate) - g.GET("/templates/:templateName/:filePath", fetchTemplateFile) - g.POST("/templates/:templateName/:filePath", updateTemplateFile) + g.GET("/templates/:templateName", fetchTemplate, validateTemplateName) + g.POST("/templates/:templateName", createOrUpdateTemplate, validateTemplateName) + g.DELETE("/templates/:templateName", deleteTemplate, validateTemplateName) + g.GET("/templates/:templateName/:filePath", fetchTemplateFile, validateTemplateName, validateTemplateFilePath) + g.POST("/templates/:templateName/:filePath", updateTemplateFile, validateTemplateName, validateTemplateFilePath) g.GET("/template-images", fetchAllTemplateImages) } diff --git a/internal/template/template.go b/internal/template/template.go index 1736aec..6dd774b 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -3,8 +3,12 @@ package template import ( "github.com/google/uuid" "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 { bun.BaseModel `bun:"table:templates,alias:template"` @@ -27,7 +31,7 @@ type templateFile struct { Content []byte `bun:"type:blob" json:"content"` } -type TemplateImage struct { +type Image struct { bun.BaseModel `bun:"table:template_images,alias:template_images"` TemplateID uuid.UUID `bun:"type:uuid" json:"-"` diff --git a/internal/template/template_manager.go b/internal/template/template_manager.go new file mode 100644 index 0000000..94dbdb0 --- /dev/null +++ b/internal/template/template_manager.go @@ -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 +} diff --git a/internal/workspace/middleware.go b/internal/workspace/middleware.go index 6be7878..3a399ae 100644 --- a/internal/workspace/middleware.go +++ b/internal/workspace/middleware.go @@ -1,8 +1,9 @@ package workspace import ( - "github.com/labstack/echo/v4" "tesseract/internal/service" + + "github.com/labstack/echo/v4" ) func newWorkspaceManagerMiddleware(services service.Services) echo.MiddlewareFunc { diff --git a/internal/workspace/workspace_manager.go b/internal/workspace/workspace_manager.go index a45a733..e490cb2 100644 --- a/internal/workspace/workspace_manager.go +++ b/internal/workspace/workspace_manager.go @@ -131,7 +131,7 @@ func (mgr workspaceManager) createWorkspace(ctx context.Context, opts createWork return nil, err } - var img template.TemplateImage + var img template.Image err = tx.NewSelect().Model(&img). Where("image_id = ?", opts.imageID). Scan(ctx) diff --git a/main.go b/main.go index 86646ea..40ec8b7 100644 --- a/main.go +++ b/main.go @@ -80,7 +80,7 @@ func main() { g := apiServer.Group("/api") workspace.DefineRoutes(g, services) - template.DefineRoutes(g) + template.DefineRoutes(g, services) apiServer.HTTPErrorHandler = func(err error, c echo.Context) { var he *echo.HTTPError