Merge remote-tracking branch 'origin/main' into feat-legacy-schema-binding
4 files changed,
+152
−0
cmd/home-placeholder/README.md+26 −0cmd/home-placeholder/main.go+32 −0cmd/home-placeholder/main_test.go+75 −0internal/home/placeholder.go+19 −0
diff --git a/cmd/home-placeholder/README.md b/cmd/home-placeholder/README.md| new file mode 100644 |
| index 0000000..7a41754 |
| --- /dev/null |
| +++ b/cmd/home-placeholder/README.md |
| @@ -0,0 +1,26 @@ |
| +# Public placeholder release |
| + |
| +Build `./cmd/home-placeholder` for the existing public skeleton at |
| +`home.bds.oncarlos.com` (including its aliases). It serves the same placeholder |
| +page and Oficina icons without opening a database or loading secrets. Activation |
| +may supply `--db`; this command deliberately ignores it. |
| + |
| +The paired credential/directory deployments continue to use `./cmd/home`. |
| +Do not deploy that binary onto the old public skeleton until its database and |
| +identity/domain migration have been planned and accepted. The old skeleton's |
| +Litestream-only database cannot be adopted by the current identity migrations. |
| + |
| +Stamp the release with the source commit in |
| +`github.com/carlosframework/rastrillo.BuildVersion`, as for the main Home binary. |
| + |
| +```sh |
| +CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath \ |
| + -ldflags "-s -w -X github.com/carlosframework/rastrillo.BuildVersion=$(git rev-parse --short HEAD)" \ |
| + -o /tmp/home-placeholder ./cmd/home-placeholder |
| +``` |
| + |
| +Run Home's full `make test` gate on Linux with the checkout on ext4 or XFS. |
| +The provisioner is Linux-only and deliberately refuses overlayfs, Btrfs, |
| +and host-shared filesystems, including ordinary Docker bind mounts on macOS. |
| +A fresh runner also needs access to the pinned private Estilo Go module, or |
| +its verified Go download cache, before starting the gate. |
diff --git a/cmd/home-placeholder/main.go b/cmd/home-placeholder/main.go| new file mode 100644 |
| index 0000000..570421e |
| --- /dev/null |
| +++ b/cmd/home-placeholder/main.go |
| @@ -0,0 +1,32 @@ |
| +// Command home-placeholder preserves the public, stateless Home skeleton while |
| +// the identity/directory database rollout is prepared separately. Never use it |
| +// for a credential or directory instance; those run cmd/home. |
| +package main |
| + |
| +import ( |
| + "log/slog" |
| + "net/http" |
| + "os" |
| + |
| + "amadan.net/oficina/home/internal/home" |
| + "github.com/carlosframework/rastrillo" |
| +) |
| + |
| +func main() { |
| + logger := slog.Default() |
| + opts, err := rastrillo.Resolve(rastrillo.Options{Logger: logger}) |
| + if err != nil { |
| + logger.Error("resolve", "err", err) |
| + os.Exit(1) |
| + } |
| + // Activation may supply --db even though this binary has no storage. |
| + // Clear it after Resolve so Serve cannot open or mutate that database. |
| + opts.DBPath = "" |
| + mux := http.NewServeMux() |
| + mux.Handle("/", home.PlaceholderHandler()) |
| + opts.Mux = mux |
| + if err := rastrillo.Serve(opts); err != nil { |
| + logger.Error("serve", "err", err) |
| + os.Exit(1) |
| + } |
| +} |
diff --git a/cmd/home-placeholder/main_test.go b/cmd/home-placeholder/main_test.go| new file mode 100644 |
| index 0000000..97e6412 |
| --- /dev/null |
| +++ b/cmd/home-placeholder/main_test.go |
| @@ -0,0 +1,75 @@ |
| +package main |
| + |
| +import ( |
| + "io" |
| + "net" |
| + "net/http" |
| + "os" |
| + "os/exec" |
| + "path/filepath" |
| + "strings" |
| + "testing" |
| + "time" |
| +) |
| + |
| +func TestPlaceholderNeverOpensActivationDatabase(t *testing.T) { |
| + dir := t.TempDir() |
| + binary := filepath.Join(dir, "home-placeholder") |
| + build := exec.Command("go", "build", "-o", binary, ".") |
| + if out, err := build.CombinedOutput(); err != nil { |
| + t.Fatalf("build: %v: %s", err, out) |
| + } |
| + l, err := net.Listen("tcp", "127.0.0.1:0") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + addr := l.Addr().String() |
| + l.Close() |
| + // The parent is deliberately missing: any database open must fail boot. |
| + database := filepath.Join(dir, "absent", "must-not-exist.db") |
| + cmd := exec.Command(binary, "-addr", addr, "-db", database) |
| + cmd.Env = []string{"PATH=" + os.Getenv("PATH")} |
| + if err := cmd.Start(); err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { _ = cmd.Process.Kill(); _ = cmd.Wait() }) |
| + client := &http.Client{Timeout: time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }} |
| + origin := "http://" + addr |
| + deadline := time.Now().Add(10 * time.Second) |
| + for { |
| + response, err := client.Get(origin + "/healthz") |
| + if err == nil { |
| + response.Body.Close() |
| + if response.StatusCode == 200 { |
| + break |
| + } |
| + } |
| + if time.Now().After(deadline) { |
| + t.Fatal("placeholder did not start without secrets and with an unusable database path") |
| + } |
| + time.Sleep(25 * time.Millisecond) |
| + } |
| + for path, status := range map[string]int{"/": 200, "/static/brand/icon.svg": 200, "/signin": 404, "/authorize": 404, "/static/brand/": 404} { |
| + response, err := client.Get(origin + path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + body, err := io.ReadAll(response.Body) |
| + response.Body.Close() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if response.StatusCode != status { |
| + t.Errorf("%s: status %d, want %d", path, response.StatusCode, status) |
| + } |
| + if len(response.Cookies()) != 0 { |
| + t.Errorf("%s set cookies", path) |
| + } |
| + if path == "/" && !strings.Contains(string(body), `href="/static/brand/icon.svg"`) { |
| + t.Error("missing favicon") |
| + } |
| + } |
| + if _, err := os.Stat(filepath.Dir(database)); !os.IsNotExist(err) { |
| + t.Fatalf("database parent created or unreadable: %v", err) |
| + } |
| +} |
diff --git a/internal/home/placeholder.go b/internal/home/placeholder.go| new file mode 100644 |
| index 0000000..e388bd4 |
| --- /dev/null |
| +++ b/internal/home/placeholder.go |
| @@ -0,0 +1,19 @@ |
| +package home |
| + |
| +import ( |
| + "net/http" |
| + |
| + "github.com/go-chi/chi/v5" |
| +) |
| + |
| +// PlaceholderHandler serves the original public landing page and approved |
| +// artwork without constructing any identity, directory, or database services. |
| +func PlaceholderHandler() http.Handler { |
| + r := chi.NewRouter() |
| + mountBrand(r) |
| + r.Get("/", func(w http.ResponseWriter, _ *http.Request) { |
| + w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| + _, _ = w.Write([]byte(indexPage)) |
| + }) |
| + return r |
| +} |