202 lines
5.3 KiB
Go
202 lines
5.3 KiB
Go
package tracking
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
type DeployLogger interface {
|
|
Printf(format string, args ...any)
|
|
}
|
|
|
|
type DeployOptions struct {
|
|
WorkerDir string
|
|
WorkerName string
|
|
DatabaseName string
|
|
TrackingKey string
|
|
AdminKey string
|
|
}
|
|
|
|
var (
|
|
errWranglerNotFound = errors.New("wrangler not found in PATH")
|
|
errWorkerConfigMissing = errors.New("worker dir missing wrangler.toml")
|
|
errParseDatabaseIDInfo = errors.New("failed to parse database_id from wrangler d1 info output")
|
|
errParseDatabaseIDCreate = errors.New("failed to parse database_id from wrangler d1 create output")
|
|
)
|
|
|
|
func DefaultWorkerName(account string) string {
|
|
sanitized := SanitizeWorkerName(account)
|
|
if sanitized == "" {
|
|
return "gog-email-tracker"
|
|
}
|
|
|
|
return "gog-email-tracker-" + sanitized
|
|
}
|
|
|
|
func SanitizeWorkerName(name string) string {
|
|
name = strings.ToLower(strings.TrimSpace(name))
|
|
if name == "" {
|
|
return ""
|
|
}
|
|
|
|
re := regexp.MustCompile(`[^a-z0-9-]+`)
|
|
name = re.ReplaceAllString(name, "-")
|
|
name = strings.Trim(name, "-")
|
|
|
|
for strings.Contains(name, "--") {
|
|
name = strings.ReplaceAll(name, "--", "-")
|
|
}
|
|
|
|
if len(name) > 63 {
|
|
name = strings.Trim(name[:63], "-")
|
|
}
|
|
|
|
return name
|
|
}
|
|
|
|
func DeployWorker(ctx context.Context, logger DeployLogger, opts DeployOptions) (string, error) {
|
|
if _, err := exec.LookPath("wrangler"); err != nil {
|
|
return "", errWranglerNotFound
|
|
}
|
|
|
|
workerDir := filepath.Clean(opts.WorkerDir)
|
|
if _, err := os.Stat(filepath.Join(workerDir, "wrangler.toml")); err != nil {
|
|
return "", fmt.Errorf("%w: %s", errWorkerConfigMissing, workerDir)
|
|
}
|
|
|
|
if logger != nil {
|
|
logger.Printf("deploy\tstarting (worker=%s, db=%s)", opts.WorkerName, opts.DatabaseName)
|
|
}
|
|
|
|
dbID, err := ensureD1Database(ctx, workerDir, opts.DatabaseName)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if runErr := runWranglerCommand(ctx, workerDir, nil, "d1", "execute", opts.DatabaseName, "--file", "schema.sql", "--remote"); runErr != nil {
|
|
return "", runErr
|
|
}
|
|
|
|
if runErr := runWranglerCommand(ctx, workerDir, strings.NewReader(opts.TrackingKey+"\n"), "secret", "put", "TRACKING_KEY", "--name", opts.WorkerName); runErr != nil {
|
|
return "", runErr
|
|
}
|
|
|
|
if runErr := runWranglerCommand(ctx, workerDir, strings.NewReader(opts.AdminKey+"\n"), "secret", "put", "ADMIN_KEY", "--name", opts.WorkerName); runErr != nil {
|
|
return "", runErr
|
|
}
|
|
|
|
configPath, err := writeWranglerConfig(workerDir, opts.WorkerName, opts.DatabaseName, dbID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer os.Remove(configPath)
|
|
|
|
if runErr := runWranglerCommand(ctx, workerDir, nil, "deploy", "--config", configPath, "--name", opts.WorkerName); runErr != nil {
|
|
return "", runErr
|
|
}
|
|
|
|
if logger != nil {
|
|
logger.Printf("deploy\tok")
|
|
}
|
|
|
|
return dbID, nil
|
|
}
|
|
|
|
func ensureD1Database(ctx context.Context, workerDir, dbName string) (string, error) {
|
|
out, err := runWranglerCommandOutput(ctx, workerDir, nil, "d1", "create", dbName)
|
|
if err != nil {
|
|
outInfo, infoErr := runWranglerCommandOutput(ctx, workerDir, nil, "d1", "info", dbName)
|
|
if infoErr != nil {
|
|
return "", err
|
|
}
|
|
|
|
id := parseDatabaseID(outInfo)
|
|
if id == "" {
|
|
return "", errParseDatabaseIDInfo
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
id := parseDatabaseID(out)
|
|
if id == "" {
|
|
return "", errParseDatabaseIDCreate
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
func parseDatabaseID(out string) string {
|
|
patterns := []*regexp.Regexp{
|
|
regexp.MustCompile(`database_id\s*=\s*\"([^\"]+)\"`),
|
|
regexp.MustCompile(`database_id\s*:\s*\"?([a-zA-Z0-9-]+)\"?`),
|
|
regexp.MustCompile(`Database ID:\s*([a-zA-Z0-9-]+)`),
|
|
}
|
|
for _, re := range patterns {
|
|
if match := re.FindStringSubmatch(out); len(match) > 1 {
|
|
return match[1]
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func writeWranglerConfig(workerDir, workerName, dbName, dbID string) (string, error) {
|
|
templatePath := filepath.Join(workerDir, "wrangler.toml")
|
|
// #nosec G304 -- path is derived from the configured worker dir
|
|
data, err := os.ReadFile(templatePath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("read wrangler.toml: %w", err)
|
|
}
|
|
|
|
content := string(data)
|
|
content = replaceTomlString(content, "name", workerName)
|
|
content = replaceTomlString(content, "database_name", dbName)
|
|
content = replaceTomlString(content, "database_id", dbID)
|
|
|
|
tmpFile, err := os.CreateTemp("", "gog-wrangler-*.toml")
|
|
if err != nil {
|
|
return "", fmt.Errorf("create temp wrangler config: %w", err)
|
|
}
|
|
defer tmpFile.Close()
|
|
|
|
if _, err := tmpFile.WriteString(content); err != nil {
|
|
return "", fmt.Errorf("write temp wrangler config: %w", err)
|
|
}
|
|
|
|
return tmpFile.Name(), nil
|
|
}
|
|
|
|
func replaceTomlString(content, key, value string) string {
|
|
re := regexp.MustCompile(fmt.Sprintf(`(?m)^%s\s*=\s*\".*\"\s*$`, regexp.QuoteMeta(key)))
|
|
return re.ReplaceAllString(content, fmt.Sprintf(`%s = \"%s\"`, key, value))
|
|
}
|
|
|
|
func runWranglerCommand(ctx context.Context, dir string, stdin io.Reader, args ...string) error {
|
|
_, err := runWranglerCommandOutput(ctx, dir, stdin, args...)
|
|
|
|
return err
|
|
}
|
|
|
|
func runWranglerCommandOutput(ctx context.Context, dir string, stdin io.Reader, args ...string) (string, error) {
|
|
cmd := exec.CommandContext(ctx, "wrangler", args...)
|
|
cmd.Dir = dir
|
|
cmd.Stdin = stdin
|
|
|
|
cmd.Env = append(os.Environ(), "WRANGLER_SEND_METRICS=false")
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return string(out), fmt.Errorf("wrangler %s failed: %w\n%s", strings.Join(args, " "), err, strings.TrimSpace(string(out)))
|
|
}
|
|
|
|
return string(out), nil
|
|
}
|