Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ spec:
```

It derives the application container, Traefik routing and TLS, release layout
under `/var/lib/ob/shop`, and retention policy. `ob canonical` prints every
under `/var/lib/onebox/app`, and retention policy. `ob canonical` prints every
derived value with its source: `# default`, `# shorthand`, or `# override`.

### 3. Plan, approve, deploy
Expand Down
35 changes: 24 additions & 11 deletions api/application/v1alpha1/application.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,17 @@
"type": "object"
},
"name": {
"description": "The application's name. Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters, and may not begin \"ob-\" or be a name the host layout reserves. Stable application name used in generated runtime identities.",
"description": "The application's name. Expects lower-case letters, digits and hyphens, starting with a letter, at most 40 characters, and may not begin \"onebox-\" or be a name the host layout reserves. Stable application name used in generated runtime identities.",
"examples": [
"shop"
],
"not": {
"anyOf": [
{
"pattern": "^ob-"
"pattern": "^onebox-"
},
{
"const": "ob"
},
{
"const": "onebox-proxy"
},
{
"const": "_host"
"const": "onebox"
}
]
},
Expand Down Expand Up @@ -221,7 +215,7 @@
"type": "object"
},
"basePath": {
"default": "/var/lib/ob",
"default": "/var/lib/onebox",
"description": "Absolute host directory beneath which Onebox stores application state and releases. Expects an absolute path with no control character or shell metacharacter.",
"examples": [
"/srv/ob"
Expand Down Expand Up @@ -926,7 +920,7 @@
"type": "boolean"
},
"network": {
"default": "ob-ingress",
"default": "onebox-ingress",
"description": "External container network shared with routed workloads; default and Onebox's derived application and service network names are reserved.",
"type": "string"
}
Expand Down Expand Up @@ -1281,6 +1275,24 @@
"description": "Also accepts the version to run."
},
"description": "Supporting services managed outside application releases, such as databases and caches.",
"propertyNames": {
"not": {
"anyOf": [
{
"const": "proxy"
},
{
"const": "discovery"
},
{
"const": "ingress"
},
{
"const": "services"
}
]
}
},
"type": "object"
},
"workloads": {
Expand Down Expand Up @@ -2107,6 +2119,7 @@
"examples": [
2
],
"maximum": 100,
"minimum": 1,
"type": "integer"
},
Expand Down
2 changes: 1 addition & 1 deletion cmd/ob/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ func connect(cmd *cobra.Command, g *globalFlags, cfg *app.Resolved, p *ctypes.Pr
// Without this the engine derives every path from the project default
// rather than the selected environment, so `ob status --env staging`
// against an environment with its own base_path reported on
// /var/lib/ob/<app> while staging lives in /srv/staging/<app>. It also
// /var/lib/onebox/app while staging lives in /srv/staging/app. It also
// leaves Environment empty on the host-ownership check, which now
// compares it.
Environment: g.Env,
Expand Down
22 changes: 22 additions & 0 deletions cmd/ob/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/signal"
"syscall"
Expand Down Expand Up @@ -105,6 +107,7 @@ func main() {
ui.RestoreCursor(os.Stderr)
stopSignals()
}()
applyTestHostStateOverride(os.Stderr)
if err := newRootCmd().ExecuteContext(ctx); err != nil {
// the one line every failure ends on — red where the terminal allows
ui.New(os.Stderr, false).Failf("ob: %v", err)
Expand All @@ -121,3 +124,22 @@ func main() {
os.Exit(2)
}
}

// applyTestHostStateOverride honours the test-only host state override, and
// says so whenever it is in the environment. With it, one host can hold more
// than one owner record, so it must never be in effect silently — least of
// all by leaking into an operator's shell or a deploy job from a test job.
func applyTestHostStateOverride(w io.Writer) (restore func()) {
restore = func() {}
value, set := os.LookupEnv(app.TestHostStateDirEnv)
if !set {
return restore
}
undo, err := app.SetTestHostStateDir(value)
if err != nil {
fmt.Fprintf(w, "warning: %s is ignored: %v\n", app.TestHostStateDirEnv, err)
return restore
}
fmt.Fprintf(w, "warning: %s=%s moves host state for a test suite; unset it on real hosts\n", app.TestHostStateDirEnv, value)
return undo
}
20 changes: 20 additions & 0 deletions cmd/ob/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"path/filepath"
"strings"
"testing"

"github.com/labstack/onebox/internal/app"
)

const mainTestProject = `apiVersion: onebox.run/v1alpha1
Expand Down Expand Up @@ -134,3 +136,21 @@ func TestExplicitProjectPathDoesNotFallback(t *testing.T) {
t.Fatalf("explicit missing ob.yml must not fall back to ob.yaml: %v", err)
}
}

func TestTestHostStateOverrideIsNeverSilent(t *testing.T) {
var out bytes.Buffer
t.Setenv(app.TestHostStateDirEnv, "/tmp/fixture-host")
t.Cleanup(applyTestHostStateOverride(&out))
if !strings.Contains(out.String(), "unset it on real hosts") {
t.Fatalf("override warning = %q", out.String())
}
if got := (app.Names{}).HostDir(); got != "/tmp/fixture-host" {
t.Fatalf("override not applied: %s", got)
}
out.Reset()
t.Setenv(app.TestHostStateDirEnv, "relative")
applyTestHostStateOverride(&out)
if !strings.Contains(out.String(), "is ignored") {
t.Fatalf("relative override warning = %q", out.String())
}
}
4 changes: 2 additions & 2 deletions cmd/ob/ops_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,10 @@ spec:
}
fake := &transport.Fake{HostName: "example.invalid", Dynamic: func(command string) (transport.Result, bool) {
switch {
case strings.HasPrefix(command, ": ob-epoch-probe;"):
case strings.HasPrefix(command, ": onebox-epoch-probe;"):
return transport.Result{ExitCode: app.ProbeAbsent}, true
case strings.Contains(command, "/_host/owner"):
return transport.Result{Stdout: "shop\n"}, true
return transport.Result{Stdout: "shop production\n"}, true
case strings.Contains(command, " logs "):
return transport.Result{Stdout: "log-secret\n", Stderr: "log-warning\n"}, true
case strings.Contains(command, "docker ps"):
Expand Down
6 changes: 3 additions & 3 deletions cmd/ob/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ func stageArtifact(final, suffix string, write func(string) error) (stagedArtifa
if err := os.MkdirAll(filepath.Dir(final), 0o755); err != nil {
return stagedArtifact{}, err
}
reserved, err := os.CreateTemp(filepath.Dir(final), filepath.Base(final)+".ob-tmp"+suffix+"-")
reserved, err := os.CreateTemp(filepath.Dir(final), filepath.Base(final)+".onebox-tmp"+suffix+"-")
if err != nil {
return stagedArtifact{}, err
}
Expand All @@ -682,7 +682,7 @@ func stageArtifact(final, suffix string, write func(string) error) (stagedArtifa
// the other kill window, between the two renames inside commit(), and
// there it is the caller's only remaining copy: deleting it would destroy
// the data this machinery exists to protect.
backup := final + ".ob-bak" + suffix
backup := final + ".onebox-bak" + suffix
orphan := false
switch {
case fileExists(final):
Expand Down Expand Up @@ -802,7 +802,7 @@ func commitArtifactSet(artifacts ...stagedArtifact) error {
// the STAGED name durable — the renames above are further directory
// changes with nothing behind them. Without this, a command can report
// success and a power loss can leave the destination absent while the
// .ob-tmp name survives.
// .onebox-tmp name survives.
//
// A sync failure rolls the set back like any other: returning it with the
// renames standing would report failure with a fresh, complete, approvable
Expand Down
2 changes: 1 addition & 1 deletion cmd/ob/preview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestPreviewRendersAndRedacts(t *testing.T) {
if err != nil {
t.Fatalf("preview failed: %v\n%s", err, out)
}
for _, want := range []string{"# digest ", "name: demo", "nginx:1.27", "ob.app: demo"} {
for _, want := range []string{"# digest ", "name: demo", "nginx:1.27", "onebox.app: demo"} {
if !strings.Contains(out, want) {
t.Errorf("missing %q\n%s", want, out)
}
Expand Down
16 changes: 8 additions & 8 deletions cmd/ob/staged_artifact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,12 +242,12 @@ func TestSameArtifactPathSeesThroughEquivalentSpellings(t *testing.T) {
// it replaced — the opposite of the guarantee.
func TestDiscardKeepsTheBackupWhenTheRestoreFailed(t *testing.T) {
dir := t.TempDir()
backup := filepath.Join(dir, "plan.json.ob-bak.plan")
backup := filepath.Join(dir, "plan.json.onebox-bak.plan")
if err := os.WriteFile(backup, []byte("previous plan"), 0o600); err != nil {
t.Fatal(err)
}
artifact := stagedArtifact{
staged: filepath.Join(dir, "plan.json.ob-tmp.plan"),
staged: filepath.Join(dir, "plan.json.onebox-tmp.plan"),
// A destination whose directory does not exist, so restore's rename
// cannot succeed.
final: filepath.Join(dir, "gone", "plan.json"),
Expand All @@ -271,12 +271,12 @@ func TestDiscardKeepsTheBackupWhenTheRestoreFailed(t *testing.T) {
func TestDiscardRemovesTheBackupWhenTheRestoreSucceeded(t *testing.T) {
dir := t.TempDir()
final := filepath.Join(dir, "plan.json")
backup := filepath.Join(dir, "plan.json.ob-bak.plan")
backup := filepath.Join(dir, "plan.json.onebox-bak.plan")
if err := os.WriteFile(backup, []byte("previous plan"), 0o600); err != nil {
t.Fatal(err)
}
artifact := stagedArtifact{
staged: filepath.Join(dir, "plan.json.ob-tmp.plan"),
staged: filepath.Join(dir, "plan.json.onebox-tmp.plan"),
final: final,
backup: backup,
replaced: true,
Expand Down Expand Up @@ -322,7 +322,7 @@ func TestStageArtifactClearsARedundantBackup(t *testing.T) {
if err := os.WriteFile(plan, []byte("current plan"), 0o600); err != nil {
t.Fatal(err)
}
stale := plan + ".ob-bak.plan"
stale := plan + ".onebox-bak.plan"
if err := os.WriteFile(stale, []byte("previous run's plan"), 0o600); err != nil {
t.Fatal(err)
}
Expand All @@ -340,7 +340,7 @@ func TestStageArtifactClearsARedundantBackup(t *testing.T) {
func TestStageArtifactKeepsABackupThatIsTheOnlyCopy(t *testing.T) {
dir := t.TempDir()
plan := filepath.Join(dir, "plan.json")
orphan := plan + ".ob-bak.plan"
orphan := plan + ".onebox-bak.plan"
if err := os.WriteFile(orphan, []byte("the only copy"), 0o600); err != nil {
t.Fatal(err)
}
Expand All @@ -363,7 +363,7 @@ func TestAFailedRunLeavesAnOrphanBackupIntact(t *testing.T) {
dir := t.TempDir()
plan := filepath.Join(dir, "plan.json")
report := filepath.Join(dir, "report.json")
orphan := plan + ".ob-bak.plan"
orphan := plan + ".onebox-bak.plan"
if err := os.WriteFile(orphan, []byte("the only copy"), 0o600); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -399,7 +399,7 @@ func TestAFailedRunLeavesAnOrphanBackupIntact(t *testing.T) {
func TestASuccessfulRunClearsAnOrphanBackup(t *testing.T) {
dir := t.TempDir()
plan := filepath.Join(dir, "plan.json")
orphan := plan + ".ob-bak.plan"
orphan := plan + ".onebox-bak.plan"
if err := os.WriteFile(orphan, []byte("previous copy"), 0o600); err != nil {
t.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/onebox-discovery/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func main() {
var socket, output, network, application string
flag.StringVar(&socket, "socket", "/var/run/docker.sock", "Docker Engine Unix socket")
flag.StringVar(&output, "output", "/dynamic/onebox.yml", "atomic Traefik dynamic configuration output")
flag.StringVar(&network, "network", "ob-ingress", "Docker network carrying routed backends")
flag.StringVar(&network, "network", "onebox-ingress", "Docker network carrying routed backends")
flag.StringVar(&application, "app", "", "Onebox Compose project to observe")
flag.Parse()
if application == "" {
Expand Down
9 changes: 5 additions & 4 deletions docs/product.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ CVE response, and an upgrade path on a host Onebox otherwise does not manage. An
operator who wants a pinned installer run inside the lock, fence and journal
boundary declares it as a bootstrap hook.

Owned application containers have one visible grammar:
`<app>-<component>-<replica>`, with a one-based replica ordinal that is never
omitted. The managed host proxy is `onebox-proxy`. These names are generated
identity, not user configuration.
Workload containers are `<app>-<component>-<replica>`, with a one-based replica
ordinal that is never omitted. Containers Onebox runs from its own images are
`onebox-<component>` with no ordinal, because none of them has replicas: the host
proxy `onebox-proxy`, and managed services such as `onebox-postgres`. These names
are generated identity, not user configuration.

The broader managed-operations goal is direction, not an inventory. Owned today:
host bootstrap, the host prerequisite check, the proxy and its TLS, the host
Expand Down
10 changes: 5 additions & 5 deletions e2e/apps/one-app-one-host.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
set -uo pipefail
export PATH="/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
APP="$1"; PORT="$2"; PATHQ="$3"; WL="${4:-}"
NAME="ob-e2e-$APP"
NAME="onebox-e2e-$APP"
S=${ONEBOX_E2E_SCRATCH:-/tmp}
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"

Expand Down Expand Up @@ -94,17 +94,17 @@ fi

# Verify from the host, not inside the container: depending on whichever of
# curl or wget an image happens to ship is not a property of the deploy.
ip=$(ssh -o BatchMode=yes "root@$IP" "docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' \$(docker ps -q --filter label=ob.app=$APP --filter label=ob.workload=$WL | head -1) 2>/dev/null | awk '{print \$1}'" 2>/dev/null | tr -d '\r')
ip=$(ssh -o BatchMode=yes "root@$IP" "docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' \$(docker ps -q --filter label=onebox.app=$APP --filter label=onebox.workload=$WL | head -1) 2>/dev/null | awk '{print \$1}'" 2>/dev/null | tr -d '\r')
code=""
for attempt in 1 2 3 4 5 6 7 8 9 10; do
code=$(ssh -o BatchMode=yes "root@$IP" "curl -s -o /dev/null -w '%{http_code}' --max-time 5 http://$ip:$PORT$PATHQ" 2>/dev/null | tr -d '\r')
case "$code" in 2*|3*) break;; esac
sleep 6
done

running=$(ssh -o BatchMode=yes "root@$IP" "docker ps -q --filter label=ob.app=$APP | wc -l" 2>/dev/null | tr -d ' ')
vols=$(ssh -o BatchMode=yes "root@$IP" "docker volume ls --format '{{.Name}}' | grep -c '^ob_' || true" 2>/dev/null | tr -d ' ')
cur=$(ssh -o BatchMode=yes "root@$IP" "readlink /var/lib/ob/$APP/current" 2>/dev/null)
running=$(ssh -o BatchMode=yes "root@$IP" "docker ps -q --filter label=onebox.app=$APP | wc -l" 2>/dev/null | tr -d ' ')
vols=$(ssh -o BatchMode=yes "root@$IP" "docker volume ls --format '{{.Name}}' | grep -c '^onebox_' || true" 2>/dev/null | tr -d ' ')
cur=$(ssh -o BatchMode=yes "root@$IP" "readlink /var/lib/onebox/app/current" 2>/dev/null)

healthy=$(echo "$out" | grep '^healthy' | sed 's/^healthy *//')
echo " ${elapsed}s http=$code containers=$running volumes=$vols healthy=[${healthy:-none declared}]"
Expand Down
6 changes: 5 additions & 1 deletion e2e/destroy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ spec:
if err := os.MkdirAll(releaseDir, 0o700); err != nil {
t.Fatal(err)
}
// What bootstrap writes; destroy deletes nothing without it.
if err := os.WriteFile(resolved.NamesFor("production").AppMarker(), []byte(application+"\n"), 0o600); err != nil {
t.Fatal(err)
}
composePath := filepath.Join(releaseDir, "compose.yaml")
envPath := filepath.Join(releaseDir, "legacy.env")
composeBody := fmt.Sprintf(`services:
Expand All @@ -68,7 +72,7 @@ volumes:
for path, body := range map[string]string{
composePath: composeBody,
envPath: "LEGACY_SECRET=recorded-value\n",
filepath.Join(releaseDir, "ob.snapshot.yml"): snapshotBody,
filepath.Join(releaseDir, "onebox.snapshot.yml"): snapshotBody,
} {
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatal(err)
Expand Down
11 changes: 2 additions & 9 deletions e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestApplicationFixturesLoad(t *testing.T) {
for _, path := range []string{
"testdata/app/ob.yml",
"testdata/worker/ob.yml",
"testdata/worker/ob-broken.yml",
"testdata/worker/broken.yml",
} {
t.Run(path, func(t *testing.T) {
// Loading validates: there is no separate step that can be
Expand All @@ -42,14 +42,7 @@ func TestApplicationFixturesLoad(t *testing.T) {
}

func TestZeroDowntimeDeploy(t *testing.T) {
if os.Getenv("ONEBOX_E2E") != "1" {
t.Skip("set ONEBOX_E2E=1 (requires local docker)")
}
// Opting in is a promise that Docker is here. Skipping past a broken daemon
// once ONEBOX_E2E=1 is set turns a gate into a green tick for work nobody did.
if err := exec.CommandContext(t.Context(), "docker", "info").Run(); err != nil {
t.Fatalf("ONEBOX_E2E=1 was set but docker is not usable: %v", err)
}
gate(t)
dir, err := filepath.Abs("testdata/app")
if err != nil {
t.Fatal(err)
Expand Down
Loading