474 lines
9.8 KiB
Go
474 lines
9.8 KiB
Go
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/docker/docker/errdefs"
|
|
"github.com/google/uuid"
|
|
"github.com/uptrace/bun"
|
|
"tesseract/internal/docker"
|
|
"time"
|
|
)
|
|
|
|
type templateManager struct {
|
|
db *bun.DB
|
|
dockerClient *client.Client
|
|
}
|
|
|
|
type createTemplateOptions struct {
|
|
baseTemplate string
|
|
name string
|
|
description string
|
|
}
|
|
|
|
type updateTemplateOptions struct {
|
|
tx *bun.Tx
|
|
|
|
// name is the new name for the template
|
|
name string
|
|
|
|
// description is the new description for the template
|
|
description string
|
|
}
|
|
|
|
type buildTemplateOptions struct {
|
|
tx *bun.Tx
|
|
imageTag string
|
|
buildArgs map[string]*string
|
|
}
|
|
|
|
var errTemplateNotFound = errors.New("template not found")
|
|
var errTemplateExists = errors.New("template already exists")
|
|
var errBaseTemplateNotFound = errors.New("base 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) findBaseTemplates(ctx context.Context) ([]baseTemplate, error) {
|
|
return baseTemplates, 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)
|
|
|
|
baseTemplate, ok := baseTemplateMap[opts.baseTemplate]
|
|
if !ok {
|
|
return nil, errBaseTemplateNotFound
|
|
}
|
|
|
|
t := template{
|
|
ID: id,
|
|
Name: opts.name,
|
|
Description: opts.description,
|
|
CreatedOn: now,
|
|
LastModifiedOn: now,
|
|
IsBuilt: false,
|
|
}
|
|
dockerfile := templateFile{
|
|
TemplateID: id,
|
|
FilePath: "Dockerfile",
|
|
Content: []byte(baseTemplate.Content),
|
|
}
|
|
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).Exec(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
t.Files = files
|
|
t.FileMap = make(map[string]*templateFile, len(files))
|
|
for _, f := range t.Files {
|
|
t.FileMap[f.FilePath] = f
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if opts.name != "" {
|
|
exists, err := tx.NewSelect().
|
|
Table("templates").
|
|
Where("name = ?", opts.name).
|
|
Exists(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if exists {
|
|
return nil, errTemplateExists
|
|
}
|
|
}
|
|
|
|
var template template
|
|
q := tx.NewUpdate().Model(&template).Where("name = ?", name)
|
|
if opts.name != "" {
|
|
q = q.Set("name = ?", opts.name)
|
|
}
|
|
if opts.description != "" {
|
|
q = q.Set("description = ?", opts.description)
|
|
}
|
|
|
|
err := q.
|
|
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 {
|
|
if errdefs.IsInvalidParameter(err) {
|
|
return nil, &errBadTemplate{
|
|
// the docker sdk returns an error message that looks like:
|
|
// "Error response from daemon: dockerfile parse error on line 1: unknown instruction: FR (did you mean FROM?)"
|
|
// we don't want the "error response..." part because it is meaningless
|
|
message: docker.CleanErrorMessage(err.Error()),
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
outputChan := make(chan any)
|
|
|
|
go func() {
|
|
defer close(outputChan)
|
|
|
|
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"
|
|
return
|
|
} 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).
|
|
On("CONFLICT DO UPDATE").
|
|
Set("image_id = EXCLUDED.image_id").
|
|
Exec(ctx)
|
|
if err != nil {
|
|
_ = tx.Rollback()
|
|
outputChan <- err
|
|
return
|
|
}
|
|
}
|
|
|
|
if autoCommit {
|
|
if err = tx.Commit(); err != nil {
|
|
_ = tx.Rollback()
|
|
outputChan <- err
|
|
return
|
|
}
|
|
}
|
|
|
|
if img != nil {
|
|
outputChan <- img
|
|
}
|
|
}()
|
|
|
|
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
|
|
}
|