Skip to content

Commit 0652726

Browse files
committed
Initial commit: launchd MVP (builder, transfer, systemd, migrate, health, tests, Makefile, README)
0 parents  commit 0652726

File tree

17 files changed

+555
-0
lines changed

17 files changed

+555
-0
lines changed

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Binaries
2+
bin/
3+
*.exe
4+
*.exe~
5+
*.dll
6+
*.so
7+
*.dylib
8+
9+
# Build artifacts
10+
build/
11+
/dist/
12+
*.out
13+
*.test
14+
15+
# Go
16+
vendor/
17+
coverage.out
18+
19+
# IDE
20+
.vscode/
21+
.idea/
22+
*.iml
23+
.DS_Store
24+
Thumbs.db

Makefile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
SHELL := /bin/bash
2+
3+
APP := launchd
4+
PKG := ./...
5+
6+
.PHONY: all build test lint clean
7+
8+
all: build
9+
10+
build:
11+
mkdir -p bin
12+
go build -o bin/$(APP) ./cmd/launchd
13+
14+
15+
test:
16+
go test -race -cover $(PKG)
17+
18+
clean:
19+
rm -rf bin
20+
rm -f coverage.out

README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# launchd
2+
Deterministic deploy orchestrator for single-binary Go services anchored to Linux systemd. launchd operationalizes a minimal, auditable deploy surface by composing battle-tested primitives: the resident Go toolchain for compilation, OpenSSH for transport and privileged control, systemd for service lifecycle, an optional migrator for schema evolution, and an HTTP health contract for liveness attestation. The objective function is predictability under duress, not novelty; the codebase is dependency-thin and tuned for reproducible behavior across heterogeneous fleet baselines.
3+
4+
## Conceptual Premise
5+
6+
The dominant substrate for “small-but-critical” services is a single daemon under systemd supervision. For these domains, heavyweight CI/CD machinery introduces variance without proportional utility. launchd prefers an austere convergence model: converge a binary onto a machine, converge a unit into systemd, converge the database state via idempotent migrations, and converge process readiness via health probes. Each convergence step is a transparent syscall to a canonical tool, yielding failure surfaces that are legible to any seasoned operator.
7+
8+
Assumptions:
9+
- A Go main package constitutes the service.
10+
- SSH reachability with privilege elevation exists to manage units.
11+
- The service exposes `/health` on a chosen TCP port.
12+
13+
## MVP Scope
14+
15+
- CLI: `launchd deploy --host <ip> --user <ssh-user> --app ./path/to/app --port <port> [--timeout <dur>]`.
16+
- Pipeline: Compile ➝ Transfer ➝ systemd Provision ➝ Migrate (optional) ➝ Health Gate.
17+
- Properties: idempotent (safe re-entrance), deterministic (explicit tooling), strict error surfacing, and minimal environmental preconditions (Go + OpenSSH).
18+
19+
### Stage Semantics
20+
- Compile: `go build -o /tmp/<app>` using the resident toolchain; no hermetic wrapper.
21+
- Transfer: `scp` to `/usr/local/bin/<app>` after ensuring parent directory ownership is sane.
22+
- systemd: materialize a unit, `daemon-reload`, `enable`, `restart`—idempotent operations.
23+
- Migrations: opportunistic `migrate up` if a migrator exists on the target PATH.
24+
- Health: poll `http://<host>:<port>/health` until success or deadline expiry.
25+
26+
## Example Usage
27+
28+
```bash
29+
launchd deploy --host 203.0.113.10 --user ubuntu --app ./examples/hello --port 8080 --timeout 60s
30+
```
31+
32+
On completion, the service is registered as `<app>.service`, executes `/usr/local/bin/<app> --port=<port>`, and is enabled for boot.
33+
34+
## Design Guarantees
35+
36+
- Determinism: all side effects mediated by explicit tools (`go`, `scp`, `ssh`, `systemctl`).
37+
- Idempotence: repeated invocations converge; non-destructive `enable`, guarded migrations.
38+
- Failure Locality: stages short-circuit with precise logging; no ambiguous partial states.
39+
- Observability: microsecond timestamps and stage banners designed for operator cognition.
40+
- Minimality: no bespoke protocol layers; everything deferential to Unix contracts.
41+
42+
## Future Roadmap
43+
44+
- Artifact integrity: checksums, content-addressed remote layout, atomic swaps.
45+
- Principle of Least Privilege: dedicated system users, hardening of unit sandboxing.
46+
- Transport hardening: native SSH client (keyboard-interactive, agent-forwarding policies) while preserving zero-daemon requirements.
47+
- Policy engines: JSON-structured logs, exponential backoff policies, retry budgets.
48+
- Migration adapters: goose, golang-migrate, app-native hooks with transactional guards.
49+
50+
## Organizational Context
51+
52+
This codebase is authored under the goVerta collective and aspires to the production rigor expected of core-infra artifacts. The project is intentionally conservative in feature accretion, biasing toward operability, determinism, and testable failure semantics over breadth.
53+
54+
## Contributors and Roles
55+
56+
- Saad H. Tiwana — lead author, deployment pipeline, systemd strategy, reliability posture
57+
GitHub: https://github.com/saadhtiwana
58+
- Ahmad Mustafa — SSH/SCP transport hardening, testing harnesses
59+
GitHub: https://github.com/ahmadmustafa02
60+
- Majid Farooq Qureshi — QA and Toolsmith, Makefile/CI touchpoints, documentation QA
61+
GitHub: https://github.com/Majid-Farooq-Qureshi
62+
63+
Saad authored the core code; Ahmad and Majid contributed engineering and QA functions across transport, tests, and docs.
64+
65+
— saad and gang is who build this

cmd/launchd/main.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"os"
7+
"path/filepath"
8+
"time"
9+
10+
"github.com/goVerta/launchd/internal/config"
11+
"github.com/goVerta/launchd/internal/executil"
12+
"github.com/goVerta/launchd/pkg/builder"
13+
"github.com/goVerta/launchd/pkg/health"
14+
"github.com/goVerta/launchd/pkg/migrate"
15+
"github.com/goVerta/launchd/pkg/systemd"
16+
"github.com/goVerta/launchd/pkg/transfer"
17+
)
18+
19+
func main() {
20+
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
21+
// saadhtiwana: deterministic, low-variance deploy orchestration entrypoint
22+
23+
if len(os.Args) < 2 {
24+
usage()
25+
os.Exit(1)
26+
}
27+
28+
sub := os.Args[1]
29+
if sub != "deploy" {
30+
usage()
31+
os.Exit(1)
32+
}
33+
34+
cfg, err := config.ParseDeployFlags(os.Args[2:], config.DeployConfig{
35+
Host: "",
36+
User: os.Getenv("USER"),
37+
AppPath: "",
38+
Port: 8080,
39+
Timeout: 60 * time.Second,
40+
})
41+
if err != nil {
42+
log.Fatalf("flag parse: %v", err)
43+
}
44+
if cfg.Host == "" || cfg.User == "" || cfg.AppPath == "" {
45+
log.Fatalf("missing required flags: --host, --user, --app")
46+
}
47+
48+
exec := executil.New()
49+
50+
appName := filepath.Base(cfg.AppPath)
51+
binOut := filepath.Join(os.TempDir(), appName)
52+
53+
log.Printf("[1/5] compile %s", cfg.AppPath)
54+
// saadhtiwana: compile using resident toolchain for portability
55+
if err := builder.Build(exec, cfg.AppPath, binOut); err != nil {
56+
log.Fatalf("build failed: %v", err)
57+
}
58+
59+
remoteBin := "/usr/local/bin/" + appName
60+
log.Printf("[2/5] transfer to %s@%s:%s", cfg.User, cfg.Host, remoteBin)
61+
// saadhtiwana: transfer artifact via OpenSSH primitives
62+
if err := transfer.SCP(exec, binOut, cfg.User, cfg.Host, remoteBin); err != nil {
63+
log.Fatalf("transfer failed: %v", err)
64+
}
65+
66+
log.Printf("[3/5] systemd service setup for %s", appName)
67+
svc := systemd.ServiceSpec{
68+
Name: appName,
69+
ExecStart: remoteBin + fmt.Sprintf(" --port=%d", cfg.Port),
70+
User: "root",
71+
After: []string{"network.target"},
72+
Environment: map[string]string{},
73+
}
74+
if err := systemd.Setup(exec, cfg.User, cfg.Host, svc); err != nil {
75+
log.Fatalf("systemd setup failed: %v", err)
76+
}
77+
78+
log.Printf("[4/5] run migrations if available")
79+
// saadhtiwana: optional DB convergence; presence-guarded
80+
if err := migrate.RunIfPresent(exec, cfg.User, cfg.Host); err != nil {
81+
log.Fatalf("migrations failed: %v", err)
82+
}
83+
84+
log.Printf("[5/5] health check http://%s:%d/health", cfg.Host, cfg.Port)
85+
// saadhtiwana: readiness gate to bound blast radius
86+
if err := health.Wait(cfg.Host, cfg.Port, cfg.Timeout); err != nil {
87+
log.Fatalf("unhealthy: %v", err)
88+
}
89+
90+
log.Printf("deploy complete")
91+
}
92+
93+
func usage() {
94+
fmt.Println("usage: launchd deploy --host <ip> --user <ssh-user> --app ./path/to/app --port <port>")
95+
}

go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module github.com/goVerta/launchd
2+
3+
go 1.22.0
4+
5+
require (
6+
)

internal/config/config.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package config
2+
3+
import (
4+
"flag"
5+
"time"
6+
)
7+
8+
type DeployConfig struct {
9+
Host string
10+
User string
11+
AppPath string
12+
Port int
13+
Timeout time.Duration
14+
}
15+
16+
func ParseDeployFlags(args []string, defaults DeployConfig) (DeployConfig, error) {
17+
fs := flag.NewFlagSet("deploy", flag.ContinueOnError)
18+
host := fs.String("host", defaults.Host, "target host")
19+
user := fs.String("user", defaults.User, "ssh user")
20+
appPath := fs.String("app", defaults.AppPath, "path to Go app (main package directory)")
21+
port := fs.Int("port", defaults.Port, "health port")
22+
timeout := fs.Duration("timeout", defaults.Timeout, "health timeout")
23+
if err := fs.Parse(args); err != nil { return DeployConfig{}, err }
24+
return DeployConfig{Host: *host, User: *user, AppPath: *appPath, Port: *port, Timeout: *timeout}, nil
25+
}

internal/executil/executil.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package executil
2+
3+
import (
4+
"bytes"
5+
"os/exec"
6+
)
7+
8+
type Executor interface {
9+
Run(name string, args ...string) error
10+
RunOutput(name string, args ...string) (string, error)
11+
}
12+
13+
type Default struct{}
14+
15+
func New() *Default { return &Default{} }
16+
17+
func (d *Default) Run(name string, args ...string) error {
18+
cmd := exec.Command(name, args...)
19+
return cmd.Run()
20+
}
21+
22+
func (d *Default) RunOutput(name string, args ...string) (string, error) {
23+
cmd := exec.Command(name, args...)
24+
var out bytes.Buffer
25+
cmd.Stdout = &out
26+
cmd.Stderr = &out
27+
err := cmd.Run()
28+
return out.String(), err
29+
}

pkg/builder/builder.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package builder
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
7+
"github.com/goVerta/launchd/internal/executil"
8+
)
9+
10+
type Builder interface {
11+
Build(appPath, out string) error
12+
}
13+
14+
type GoBuilder struct{ Exec executil.Executor }
15+
16+
func Build(exec executil.Executor, appPath, out string) error {
17+
if exec == nil { return fmt.Errorf("nil executor") }
18+
abs, err := filepath.Abs(appPath)
19+
if err != nil { return err }
20+
// saadhtiwana: build using system go toolchain for determinism
21+
return exec.Run("go", "build", "-o", out, abs)
22+
}

pkg/builder/builder_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package builder
2+
3+
import (
4+
"errors"
5+
"reflect"
6+
"testing"
7+
)
8+
9+
type fakeExec struct{ calls [][]string; err error }
10+
11+
func (f *fakeExec) Run(name string, args ...string) error {
12+
f.calls = append(f.calls, append([]string{name}, args...))
13+
return f.err
14+
}
15+
func (f *fakeExec) RunOutput(name string, args ...string) (string, error) { return "", nil }
16+
17+
func TestBuild_InvokesGoBuild(t *testing.T) {
18+
ex := &fakeExec{}
19+
if err := Build(ex, "./testdata/app", "/tmp/out"); err != nil {
20+
t.Fatalf("unexpected: %v", err)
21+
}
22+
if len(ex.calls) != 1 { t.Fatalf("calls=%d", len(ex.calls)) }
23+
got := ex.calls[0]
24+
if got[0] != "go" || !reflect.DeepEqual(got[1:3], []string{"build", "-o"}) {
25+
t.Fatalf("bad invocation: %v", got)
26+
}
27+
}
28+
29+
func TestBuild_ErrorSurfaced(t *testing.T) {
30+
ex := &fakeExec{err: errors.New("boom")}
31+
if err := Build(ex, "./app", "/tmp/out"); err == nil {
32+
t.Fatalf("expected error")
33+
}
34+
}

pkg/health/health.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package health
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"time"
9+
)
10+
11+
func Wait(host string, port int, timeout time.Duration) error {
12+
client := &http.Client{ Timeout: 5 * time.Second }
13+
deadline := time.Now().Add(timeout)
14+
url := fmt.Sprintf("http://%s:%d/health", host, port)
15+
for {
16+
if time.Now().After(deadline) { return errors.New("timeout waiting for health") }
17+
resp, err := client.Get(url)
18+
if err == nil {
19+
b, _ := io.ReadAll(resp.Body)
20+
_ = resp.Body.Close()
21+
if resp.StatusCode >= 200 && resp.StatusCode < 300 { return nil }
22+
_ = b
23+
}
24+
time.Sleep(500 * time.Millisecond)
25+
}
26+
}

0 commit comments

Comments
 (0)