service.go

115 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
package scaffold

import (
	"cmp"
	"fmt"
	"log"
	"strings"

	"congo.gg/pkg/platform"
)

func startService(server *platform.Server, name string, spec *ServiceSpec, imageName string, serverVolumes []VolumeSpec) error {
	log.Printf("   Starting %s...", name)

	// Build volume name → mount path lookup for $volume-name expansion
	volMounts := make(map[string]string, len(serverVolumes))
	for _, v := range serverVolumes {
		volMounts[v.Name] = v.Mount
	}
	expandRef := func(s string) string {
		if !strings.HasPrefix(s, "$") {
			return s
		}
		ref := s[1:]
		rest := ""
		if idx := strings.Index(ref, "/"); idx >= 0 {
			rest = ref[idx:]
			ref = ref[:idx]
		}
		if mount, ok := volMounts[ref]; ok {
			return mount + rest
		}
		return s
	}

	svc := &platform.Service{
		Name:       name,
		Image:      cmp.Or(imageName, spec.Image),
		Network:    spec.Network,
		Restart:    "always",
		Privileged: spec.Privileged,
	}

	if len(spec.Command) > 0 {
		svc.Command = spec.Command
	}

	for _, p := range spec.Ports {
		svc.Ports = append(svc.Ports, platform.Port{
			Host:      p.Host,
			Container: p.Container,
			Bind:      p.Bind,
		})
	}

	for _, v := range spec.Volumes {
		svc.Volumes = append(svc.Volumes, platform.Mount{Source: expandRef(v.Source), Target: v.Target})
	}

	svc.Env = make(map[string]string)
	for k, v := range spec.Env {
		svc.Env[k] = expandRef(v)
	}

	for envName, filePath := range spec.EnvFiles {
		if !SafePath(filePath) {
			log.Printf("   Warning: skipping env_file %s — invalid path %q", envName, filePath)
			continue
		}
		value, err := server.SSH("cat", filePath)
		if err != nil {
			log.Printf("   Warning: could not read %s from %s", envName, filePath)
			continue
		}
		svc.Env[envName] = strings.TrimSpace(value)
	}

	svc.Logging = &platform.LogConfig{
		Driver: "json-file",
		Options: map[string]string{
			"max-size": "50m",
			"max-file": "3",
		},
	}

	var healthCmd string
	if spec.Healthcheck != "" {
		healthCmd = spec.Healthcheck
	} else if len(spec.Ports) > 0 {
		healthCmd = fmt.Sprintf("curl -sf http://localhost:%d/health || exit 1", spec.Ports[0].Container)
	}
	if healthCmd != "" {
		svc.Healthcheck = &platform.Healthcheck{
			Cmd:         healthCmd,
			Interval:    "10s",
			Timeout:     "5s",
			Retries:     3,
			StartPeriod: "30s",
		}
	}

	if err := server.Start(svc); err != nil {
		return err
	}

	if svc.Healthcheck != nil {
		if err := server.WaitForHealthy(name, 60); err != nil {
			log.Printf("   Health check failed, rolling back...")
			server.Rollback(name)
			return fmt.Errorf("health check failed: %w", err)
		}
	}

	return nil
}