beehive/deployment.go

223 lines
5.0 KiB
Go

package beehive
import (
"bufio"
"bytes"
"fmt"
"io/fs"
"log"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
)
type Deployment struct {
ID int
// Client this deployment belongs to.
Client int
Festoon string
// User ID for file permissions.
UID int
// Ports in use.
Ports []int
Worker *Worker
}
var replacementPort = regexp.MustCompile(`(HOSTALGIA_PORT_[A-Z])`)
var replacementPassword = regexp.MustCompile(`(HOSTALGIA_PASSWORD_[A-Z])`)
const defaultName = "hostalgia.net dedicated server"
func (d *Deployment) Interpolate(filePath string, customValues map[string]string) ([]byte, error) {
buf, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
replacements := map[string]string{
"HOSTALGIA_ID": fmt.Sprintf("%s-%d", d.Festoon, d.ID),
"HOSTALGIA_IP": d.Worker.IP,
"HOSTALGIA_QUOTED_NAME": defaultName,
"HOSTALGIA_NAME": defaultName,
}
// TODO passwords
for customKey, customValue := range customValues {
replacements[customKey] = customValue
}
replacements["HOSTALGIA_QUOTED_NAME"] = strings.ReplaceAll(replacements["HOSTALGIA_QUOTED_NAME"], `"`, `\"`)
for original, replacement := range replacements {
buf = bytes.ReplaceAll(buf, []byte(original), []byte(replacement))
}
configPorts := make(map[string]bool)
matches := replacementPort.FindAll(buf, -1)
for _, match := range matches {
configPorts[string(match)] = true
}
buf = replacementPort.ReplaceAllFunc(buf, func(i []byte) []byte {
index := int(i[len(i)-1:][0] - 'A')
if index > len(d.Ports) {
log.Fatalf("failed to Interpolate %s: insufficient ports", i)
}
return []byte(strconv.Itoa(d.Ports[index]))
})
buf = replacementPassword.ReplaceAllFunc(buf, func(i []byte) []byte {
// TODO
return i
})
return buf, nil
}
func (d *Deployment) interpolateAndCopy(inFile string, outFile string) error {
data, err := d.Interpolate(inFile, nil)
if err != nil {
return err
}
return os.WriteFile(outFile, data, 0600)
}
func (d *Deployment) deploy() error {
if strings.TrimSpace(d.Festoon) == "" {
return fmt.Errorf("unknown festoon: %s", d.Festoon)
}
match, err := regexp.MatchString(`^[a-zA-Z0-9]+$`, d.Festoon)
if err != nil {
return err
} else if !match {
return fmt.Errorf("unknown festoon: %s", d.Festoon)
}
festoonPath := path.Join(d.Worker.FestoonsDir, d.Festoon)
copyDataDir := path.Join(festoonPath, "data")
fileInfo, err := os.Stat(d.Dir())
if err != nil {
if !os.IsNotExist(err) {
return err
}
err = os.MkdirAll(d.Dir(), 0700)
if err != nil {
return err
}
} else if !fileInfo.IsDir() {
return fmt.Errorf("invalid output directory: %s", d.Dir())
}
err = d.interpolateAndCopy(path.Join(festoonPath, "docker-compose.yml"), path.Join(d.Dir(), "docker-compose.yml"))
if err != nil {
return err
}
outDataPath := path.Join(d.Dir(), "data")
fileInfo, err = os.Stat(copyDataDir)
if err != nil {
if !os.IsNotExist(err) {
return err
}
} else if fileInfo.IsDir() {
err = filepath.WalkDir(copyDataDir, func(filePath string, dirEntry fs.DirEntry, err error) error {
relativePath := strings.TrimPrefix(filePath, copyDataDir)
if relativePath == "" {
return err
} else if !strings.HasPrefix(relativePath, "/") {
log.Fatalf("unexpected file path: %s", relativePath)
}
relativePath = relativePath[1:]
outPath := path.Join(outDataPath, relativePath)
if dirEntry.IsDir() {
err = os.MkdirAll(outPath, 0700)
if err != nil {
log.Fatal(err)
}
return err
}
return d.interpolateAndCopy(filePath, outPath)
})
if err != nil {
return err
}
}
stdOut, stdErr, err := DockerCompose(d.Dir(), []string{"up", "-d"})
if bytes.Contains(stdErr, []byte(fmt.Sprintf("%s is up-to-date", d.Label()))) {
log.Printf("Warning: %s was already up", d.Label())
} else if err != nil {
return fmt.Errorf("failed to bring deployment up: %s", err)
}
log.Printf("docker compose stdOut: %s", stdOut)
log.Printf("docker compose stdErr: %s", stdErr)
log.Println("deployment UP!")
return nil
}
func (d *Deployment) Label() string {
return fmt.Sprintf("%s-%d", d.Festoon, d.ID)
}
func (d *Deployment) Dir() string {
return path.Join(d.Worker.DeploymentsDir, d.Label())
}
func (d *Deployment) handleEvents() {
log.Println("HANDLE EVENTS")
cmd, stdOut, stdErr, err := DockerEvents(d.Label())
if err != nil {
log.Fatal(err, stdErr.String())
}
go func() {
for {
scanner := bufio.NewScanner(stdErr)
for scanner.Scan() {
log.Println("EVENT", string(scanner.Bytes()))
}
if scanner.Err() != nil {
log.Fatal("scanner error", scanner.Err())
}
time.Sleep(2 * time.Millisecond)
}
}()
_ = cmd
for {
scanner := bufio.NewScanner(stdOut)
for scanner.Scan() {
b := scanner.Bytes()
if !bytes.HasPrefix(b, []byte("H.net")) || !bytes.HasSuffix(b, []byte("H.net")) {
log.Fatalf("unrecognized event: %s", b)
}
l := len(b)
if l == 10 {
continue
}
log.Println("Container status", string(b[5:l-5]))
}
if scanner.Err() != nil {
log.Fatal("scanner error", scanner.Err())
}
time.Sleep(2 * time.Millisecond)
}
}