| 1 | package activator |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | "io/fs" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "strings" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/hostbox" |
| 12 | "github.com/carlosframework/platform/internal/instancectl" |
| 13 | "github.com/carlosframework/platform/internal/registry" |
| 14 | ) |
| 15 | |
| 16 | // ClaimLaunchRoles are the processes the real start of rt spawns: main |
| 17 | // always, the route's first-boot seed when it has one, and the legacy |
| 18 | // co-spawned sidecar when its env file is staged. The isolated claim launch |
| 19 | // carries main alone. |
| 20 | func (b *ExecBackend) ClaimLaunchRoles(rt registry.Route) ([]string, error) { |
| 21 | return b.claimLaunchRoles(rt, os.Stat) |
| 22 | } |
| 23 | |
| 24 | func (b *ExecBackend) claimLaunchRoles(rt registry.Route, stat func(string) (os.FileInfo, error)) ([]string, error) { |
| 25 | roles := []string{"main"} |
| 26 | if rt.Seed != "" { |
| 27 | roles = append(roles, "seed") |
| 28 | } |
| 29 | _, err := stat(b.pathsFor(rt).sidecarEnv) |
| 30 | switch { |
| 31 | case err == nil: |
| 32 | roles = append(roles, "sidecar") |
| 33 | case errors.Is(err, fs.ErrNotExist): |
| 34 | // A proven absence is the only safe negative answer. Other inspection |
| 35 | // failures cannot admit a launch whose complete role set is unknown. |
| 36 | default: |
| 37 | return nil, fmt.Errorf("inspect staged sidecar for %s: %w", rt.Host, err) |
| 38 | } |
| 39 | return roles, nil |
| 40 | } |
| 41 | |
| 42 | // claimLaunchShape refuses, read-only, a route whose real start is more than |
| 43 | // the isolated main launch. |
| 44 | func (b *ExecBackend) claimLaunchShape(rt registry.Route) error { |
| 45 | roles, err := b.ClaimLaunchRoles(rt) |
| 46 | if err != nil { |
| 47 | return err |
| 48 | } |
| 49 | if len(roles) != 1 { |
| 50 | return hostbox.ErrUnsupported |
| 51 | } |
| 52 | return nil |
| 53 | } |
| 54 | |
| 55 | func isolatedCleanAbs(p string) bool { |
| 56 | return filepath.IsAbs(p) && filepath.Clean(p) == p && !strings.ContainsRune(p, 0) |
| 57 | } |
| 58 | |
| 59 | // IsolatedArgv validates the complete isolated launch shape and returns the |
| 60 | // trampoline's argv: the unit's view of the host's live build, then exactly |
| 61 | // --socket in the run directory and --db in the instance directory, each |
| 62 | // once. args excludes argv[0]. env must be well-formed (KEY=value, no NUL, |
| 63 | // no duplicate key) and carry the instance's admin token. It is the one |
| 64 | // validator for startIsolated and the claim preflight. |
| 65 | func IsolatedArgv(host, launchDir, runDir string, args, env []string) (argv []string, socket, db string, err error) { |
| 66 | if len(args)%2 != 0 || !isolatedCleanAbs(launchDir) || !isolatedCleanAbs(runDir) { |
| 67 | return nil, "", "", hostbox.ErrRoleLaunchShape |
| 68 | } |
| 69 | for i := 0; i < len(args); i += 2 { |
| 70 | value := args[i+1] |
| 71 | switch { |
| 72 | case args[i] == "--socket" && socket == "" && isolatedCleanAbs(value) && filepath.Dir(value) == runDir: |
| 73 | socket = value |
| 74 | case args[i] == "--db" && db == "" && isolatedCleanAbs(value) && filepath.Dir(value) == launchDir: |
| 75 | db = value |
| 76 | default: |
| 77 | return nil, "", "", hostbox.ErrRoleLaunchShape |
| 78 | } |
| 79 | } |
| 80 | if socket == "" || db == "" { |
| 81 | return nil, "", "", hostbox.ErrRoleLaunchShape |
| 82 | } |
| 83 | seen := make(map[string]bool, len(env)) |
| 84 | for _, kv := range env { |
| 85 | k, _, ok := strings.Cut(kv, "=") |
| 86 | if !ok || k == "" || strings.ContainsRune(kv, 0) || seen[k] { |
| 87 | return nil, "", "", hostbox.ErrRoleLaunchShape |
| 88 | } |
| 89 | seen[k] = true |
| 90 | } |
| 91 | if !seen["CARLOS_ADMIN_TOKEN"] || !envHas(env, "CARLOS_ADMIN_TOKEN=") { |
| 92 | return nil, "", "", hostbox.ErrRoleLaunchShape |
| 93 | } |
| 94 | return append([]string{filepath.Join(instancectl.LiveDir, host)}, args...), socket, db, nil |
| 95 | } |
| 96 | |
| 97 | // envHas reports a non-empty value for the given "KEY=" prefix. |
| 98 | func envHas(env []string, prefix string) bool { |
| 99 | for _, kv := range env { |
| 100 | if v, ok := strings.CutPrefix(kv, prefix); ok && v != "" { |
| 101 | return true |
| 102 | } |
| 103 | } |
| 104 | return false |
| 105 | } |
| 106 | |