docs: publish Tend Compose continuity evidence
Export the reviewed allowlisted snapshot from private source commit 07c1655921f21ee5e4fc4d85639d199e8867b17d. This records the Docker Compose activation, schema-compatible rollback, and stateful migration resource findings from Observatory Preview 19 dogfooding. AI-Assisted: OpenAI Codex Signed-off-by: Cole Speelman <crspeelman@gmail.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
Zero-Clause BSD
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Reusable Tend examples
|
||||
|
||||
This subtree is licensed 0BSD so an operator can copy and adapt it without
|
||||
bringing the Tend program's AGPL license into an application configuration.
|
||||
|
||||
The blue/green example expects one separately reviewed environment file that
|
||||
the two installed slots and transient validation use consistently:
|
||||
|
||||
```text
|
||||
# /etc/tend/environment/example-site.env
|
||||
APP_SECRET=replace-on-server
|
||||
```
|
||||
|
||||
The blue/green systemd slot units then read the nonsecret listen address from
|
||||
`/etc/tend/slots/example-site-blue.env` or `-green.env`; Tend overrides only
|
||||
the isolated candidate address. Secret values never enter `tend.json`.
|
||||
|
||||
The singleton example follows the same split: its shared root-only environment
|
||||
file omits the configured listen key, the installed unit owns the live address,
|
||||
and Tend supplies only the transient candidate address. This prevents a shared
|
||||
environment file from overriding the isolated candidate port. Its imported
|
||||
Caddy handler is also managed from one reviewed template. Tend temporarily
|
||||
routes the canonical origin to the proven candidate while the fixed-address
|
||||
unit restarts, then returns traffic to that unit only after it passes its local
|
||||
checks.
|
||||
|
||||
Production configuration belongs outside the source checkout, owned by root,
|
||||
and not group- or world-writable. The Caddy handler template is an entire
|
||||
imported handler fragment; the enclosing site, matchers, and routing precedence
|
||||
remain operator-owned.
|
||||
|
||||
`event_log` is a per-service, root-owned JSONL evidence stream below that
|
||||
service's release root. Grant an Observatory agent read access explicitly; do
|
||||
not make the release root broadly readable. `activation_window_seconds` keeps
|
||||
canonical routed probes active after Caddy reload and keeps the previous
|
||||
blue/green slot—or the singleton handoff candidate—under health/readiness
|
||||
observation until the activation is recorded.
|
||||
|
||||
`server/` demonstrates the schema-2 receive policy, forced OpenSSH command,
|
||||
restricted sudo entry, two independent service configurations, and secret-file
|
||||
placement. The values are placeholders, not an installation script.
|
||||
@@ -0,0 +1,2 @@
|
||||
# Managed by Tend. The enclosing site and route matchers remain operator-owned.
|
||||
reverse_proxy {{UPSTREAM}}
|
||||
@@ -0,0 +1,18 @@
|
||||
[Unit]
|
||||
Description=Example site (%i)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
DynamicUser=yes
|
||||
ExecStart=/opt/example-site/slots/%i/example-site
|
||||
EnvironmentFile=/etc/tend/environment/example-site.env
|
||||
EnvironmentFile=/etc/tend/slots/example-site-%i.env
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"service": { "name": "example-site", "allowed_host": "example.test", "environment_file": "/etc/tend/environment/example-site.env" },
|
||||
"build": { "package": "./cmd/site", "binary": "example-site", "branch": "main" },
|
||||
"deployment": {
|
||||
"strategy": "blue_green",
|
||||
"root": "/opt/example-site",
|
||||
"lock_file": "/run/lock/tend-deploy.lock",
|
||||
"state_file": "/opt/example-site/tend-state.json",
|
||||
"event_log": "/opt/example-site/deployment-events.jsonl",
|
||||
"health_path": "/healthz",
|
||||
"readiness_path": "/readyz",
|
||||
"candidate_timeout_seconds": 30,
|
||||
"activation_window_seconds": 10,
|
||||
"smoke": [{ "path": "/", "contains": "Example site" }],
|
||||
"public_smoke": [{ "url": "https://example.test/", "contains": "Example site" }],
|
||||
"blue_green": {
|
||||
"caddy_config": "/etc/caddy/Caddyfile",
|
||||
"caddy_handler": "/etc/caddy/example-site-handler.caddy",
|
||||
"caddy_handler_template": "/etc/example-site/caddy-handler.template",
|
||||
"bootstrap_active": "blue",
|
||||
"blue": { "unit": "example-site-blue.service", "address": "127.0.0.1:8090", "link": "/opt/example-site/slots/blue" },
|
||||
"green": { "unit": "example-site-green.service", "address": "127.0.0.1:8091", "link": "/opt/example-site/slots/green" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
APP_MODE=development
|
||||
APP_SECRET=replace-with-a-local-random-value
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
restrict,command="sudo -n /usr/local/bin/tend receive --policy /etc/tend/receive-policy.json" ssh-ed25519 REPLACE_WITH_DEPLOY_KEY tend-deploy
|
||||
@@ -0,0 +1,2 @@
|
||||
# Managed by Tend. The enclosing site and route matchers remain operator-owned.
|
||||
reverse_proxy {{UPSTREAM}}
|
||||
@@ -0,0 +1,2 @@
|
||||
# Managed by Tend. The enclosing site and route matchers remain operator-owned.
|
||||
reverse_proxy {{UPSTREAM}}
|
||||
@@ -0,0 +1,3 @@
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
DOCS_LISTEN=127.0.0.1:8102
|
||||
APP_SECRET=replace-on-server
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
APP_SECRET=replace-on-server
|
||||
@@ -0,0 +1,19 @@
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
[Unit]
|
||||
Description=Example singleton site
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
DynamicUser=yes
|
||||
ExecStart=/opt/example-site/current/example-site
|
||||
EnvironmentFile=/etc/tend/environment/example-site.env
|
||||
Environment=EXAMPLE_LISTEN=127.0.0.1:8092
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"config_root": "/etc/tend/services",
|
||||
"incoming_root": "/var/lib/tend/incoming",
|
||||
"shared_lock_file": "/run/lock/tend-deploy.lock",
|
||||
"services": {
|
||||
"docs-site": {
|
||||
"config": "/etc/tend/services/docs-site.json",
|
||||
"max_artifact_bytes": 134217728
|
||||
},
|
||||
"example-site": {
|
||||
"config": "/etc/tend/services/example-site.json",
|
||||
"max_artifact_bytes": 134217728
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"service": { "name": "docs-site", "allowed_host": "docs.example.test", "environment_file": "/etc/tend/environment/docs-site.env" },
|
||||
"build": { "package": "./cmd/docs", "binary": "docs-site", "branch": "main" },
|
||||
"deployment": {
|
||||
"strategy": "singleton_candidate",
|
||||
"root": "/opt/docs-site",
|
||||
"lock_file": "/run/lock/tend-deploy.lock",
|
||||
"state_file": "/opt/docs-site/tend-state.json",
|
||||
"event_log": "/opt/docs-site/deployment-events.jsonl",
|
||||
"health_path": "/healthz",
|
||||
"readiness_path": "/readyz",
|
||||
"candidate_timeout_seconds": 30,
|
||||
"activation_window_seconds": 10,
|
||||
"smoke": [{ "path": "/", "contains": "Documentation" }],
|
||||
"public_smoke": [{ "url": "https://docs.example.test/", "contains": "Documentation" }],
|
||||
"singleton": {
|
||||
"unit": "docs-site.service",
|
||||
"address": "127.0.0.1:8102",
|
||||
"candidate_address": "127.0.0.1:18102",
|
||||
"listen_env": "DOCS_LISTEN",
|
||||
"current_link": "/opt/docs-site/current",
|
||||
"previous_link": "/opt/docs-site/previous",
|
||||
"caddy_config": "/etc/caddy/Caddyfile",
|
||||
"caddy_handler": "/etc/caddy/docs-site-handler.caddy",
|
||||
"caddy_handler_template": "/etc/tend/caddy/docs-site.template"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"service": { "name": "example-site", "allowed_host": "example.test", "environment_file": "/etc/tend/environment/example-site.env" },
|
||||
"build": { "package": "./cmd/site", "binary": "example-site", "branch": "main" },
|
||||
"deployment": {
|
||||
"strategy": "singleton_candidate",
|
||||
"root": "/opt/example-site",
|
||||
"lock_file": "/run/lock/tend-deploy.lock",
|
||||
"state_file": "/opt/example-site/tend-state.json",
|
||||
"event_log": "/opt/example-site/deployment-events.jsonl",
|
||||
"health_path": "/healthz",
|
||||
"readiness_path": "/readyz",
|
||||
"candidate_timeout_seconds": 30,
|
||||
"activation_window_seconds": 10,
|
||||
"smoke": [{ "path": "/", "contains": "Example site" }],
|
||||
"public_smoke": [{ "url": "https://example.test/", "contains": "Example site" }],
|
||||
"singleton": {
|
||||
"unit": "example-site.service",
|
||||
"address": "127.0.0.1:8092",
|
||||
"candidate_address": "127.0.0.1:18092",
|
||||
"listen_env": "EXAMPLE_LISTEN",
|
||||
"current_link": "/opt/example-site/current",
|
||||
"previous_link": "/opt/example-site/previous",
|
||||
"caddy_config": "/etc/caddy/Caddyfile",
|
||||
"caddy_handler": "/etc/caddy/example-site-handler.caddy",
|
||||
"caddy_handler_template": "/etc/tend/caddy/example-site.template"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
EXAMPLE_LISTEN=127.0.0.1:8090
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
EXAMPLE_LISTEN=127.0.0.1:8091
|
||||
@@ -0,0 +1,3 @@
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
Defaults:tend-deploy env_keep += "SSH_ORIGINAL_COMMAND"
|
||||
tend-deploy ALL=(root) NOPASSWD: /usr/local/bin/tend receive --policy /etc/tend/receive-policy.json
|
||||
@@ -0,0 +1,2 @@
|
||||
# Managed by Tend. The enclosing site and route matchers remain operator-owned.
|
||||
reverse_proxy {{UPSTREAM}}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"service": { "name": "example-site", "allowed_host": "example.test", "environment_file": "/etc/tend/environment/example-site.env" },
|
||||
"build": { "package": "./cmd/site", "binary": "example-site", "branch": "main" },
|
||||
"deployment": {
|
||||
"strategy": "singleton_candidate",
|
||||
"root": "/opt/example-site",
|
||||
"lock_file": "/run/lock/tend-deploy.lock",
|
||||
"state_file": "/opt/example-site/tend-state.json",
|
||||
"event_log": "/opt/example-site/deployment-events.jsonl",
|
||||
"health_path": "/healthz",
|
||||
"readiness_path": "/readyz",
|
||||
"candidate_timeout_seconds": 30,
|
||||
"activation_window_seconds": 10,
|
||||
"smoke": [{ "path": "/", "contains": "Example site" }],
|
||||
"public_smoke": [{ "url": "https://example.test/", "contains": "Example site" }],
|
||||
"singleton": {
|
||||
"unit": "example-site.service",
|
||||
"address": "127.0.0.1:8092",
|
||||
"candidate_address": "127.0.0.1:18092",
|
||||
"listen_env": "EXAMPLE_LISTEN",
|
||||
"current_link": "/opt/example-site/current",
|
||||
"previous_link": "/opt/example-site/previous",
|
||||
"caddy_config": "/etc/caddy/Caddyfile",
|
||||
"caddy_handler": "/etc/caddy/example-site-handler.caddy",
|
||||
"caddy_handler_template": "/etc/tend/caddy/example-site.template"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user