5 Commits
Author SHA1 Message Date
VeillaxandClaude Sonnet 5.5 17c222c946 Report update progress and log to Hammerhead while updating
CI / test (push) Successful in 4m52s
Release / release (push) Successful in 7m36s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 17:58:37 -06:00
VeillaxandClaude Sonnet 5.5 2943af9bbf Report version and auto-update state to Hammerhead and update on request
CI / test (push) Successful in 8m7s
Release / release (push) Successful in 4m42s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 17:03:38 -06:00
VeillaxandClaude Sonnet 5.5 e3353f65da Retry release uploads that hit a proxy timeout, and replace every existing asset on re-run
CI / test (push) Successful in 5m13s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 14:57:03 -06:00
VeillaxandClaude Sonnet 5.5 a30afb1026 Fix the staged install link used by DESTDIR
CI / test (push) Failing after 16m57s
Release / release (push) Successful in 6m13s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 14:01:46 -06:00
VeillaxandClaude Sonnet 5.5 3dc8d1af46 Add opt-in auto-update from Gitea releases
CI / test (push) Successful in 5m57s
Release / release (push) Failing after 19m45s
The program now lives in /usr/local/lib/bamboo and is owned by its service user so it can replace itself.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 14:00:42 -06:00
9 changed files with 154 additions and 18 deletions

No files matched your search

+4
View File
@@ -9,3 +9,7 @@ BAMBOO_TOKEN=
# Folder for this agent's WireGuard key. # Folder for this agent's WireGuard key.
#BAMBOO_DATA=./bamboo-data #BAMBOO_DATA=./bamboo-data
# Check for new releases every few hours and update and restart by itself (Linux and macOS).
# setup.sh turns this on; remove it to update by hand.
#BAMBOO_AUTO_UPDATE=1
+4
View File
@@ -48,6 +48,10 @@ sudo systemctl start bamboo
Use `linux_arm64` or `linux_arm` for Raspberry Pi and other ARM machines. To upgrade, repeat the same steps: your settings and data are kept. `sudo ./install.sh --uninstall` removes the service and binary but leaves `/etc/bamboo` and `/var/lib/bamboo` alone. `bamboo -version` tells you which build is running. Use `linux_arm64` or `linux_arm` for Raspberry Pi and other ARM machines. To upgrade, repeat the same steps: your settings and data are kept. `sudo ./install.sh --uninstall` removes the service and binary but leaves `/etc/bamboo` and `/var/lib/bamboo` alone. `bamboo -version` tells you which build is running.
**Updates**
Set `BAMBOO_AUTO_UPDATE=1` (or pass `-auto-update`) and Bamboo checks this server for a newer release every few hours, verifies it against `SHA256SUMS`, checks that the new binary runs, swaps it in and restarts itself. It only follows full releases (never pre-releases), never downgrades, and keeps the old binary if anything looks wrong. `setup.sh` turns it on. It is not available on Windows, and it is for the systemd and plain-binary installs: with Docker, pull a new image instead.
**With Docker** **With Docker**
``` ```
+92 -1
View File
@@ -21,6 +21,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"git.veillax.com/Veillax/reef/docker" "git.veillax.com/Veillax/reef/docker"
@@ -34,6 +35,11 @@ type Config struct {
Token string Token string
DataDir string // holds this agent's WireGuard key DataDir string // holds this agent's WireGuard key
Docker bool // report local Docker containers so they can be picked as targets Docker bool // report local Docker containers so they can be picked as targets
// Version, AutoUpdate and UpdateNow work as in blacktip.Config.
Version string
AutoUpdate bool
UpdateNow func(ctx context.Context, progress func(stage string, pct int, line string)) (newVersion string, err error)
} }
type Bamboo struct { type Bamboo struct {
@@ -50,6 +56,14 @@ type Bamboo struct {
edges []model.EdgeRef edges []model.EdgeRef
lastRep string lastRep string
updating atomic.Bool
updateMsg string // outcome of the update Hammerhead asked for, reported back
updateID string // the request that updateMsg answers
updStage string // progress of the running update, reported to Hammerhead
updPct int
updLog []string
updSent time.Time
mail map[string]*mailRun mail map[string]*mailRun
mailErr map[string]string // mail egress ID -> "" (listening) or an error mailErr map[string]string // mail egress ID -> "" (listening) or an error
} }
@@ -143,6 +157,9 @@ func (b *Bamboo) apply(cfg *model.SiteConfig) {
b.mu.Lock() b.mu.Lock()
b.version = cfg.Version b.version = cfg.Version
b.edges = cfg.Edges b.edges = cfg.Edges
if cfg.UpdateID != "" {
b.requestUpdate(cfg.UpdateID)
}
if cfg.TunnelIP != b.tunIP && b.tun != nil { if cfg.TunnelIP != b.tunIP && b.tun != nil {
b.tun.Close() b.tun.Close()
b.tun = nil b.tun = nil
@@ -246,7 +263,11 @@ func (b *Bamboo) report() {
containers, _ = docker.List(ctx) containers, _ = docker.List(ctx)
cancel() cancel()
} }
body, _ := json.Marshal(model.SiteReport{PublicKey: b.wgPub, Connected: ids, Containers: containers, MailEgress: b.mailStatus()}) b.mu.Lock()
msg, uid, stage, pct, ulog := b.updateMsg, b.updateID, b.updStage, b.updPct, append([]string(nil), b.updLog...)
b.mu.Unlock()
body, _ := json.Marshal(model.SiteReport{PublicKey: b.wgPub, Connected: ids, Containers: containers, MailEgress: b.mailStatus(),
Version: b.cfg.Version, AutoUpdate: b.cfg.AutoUpdate, UpdateMsg: msg, UpdateID: uid, UpdateStage: stage, UpdatePct: pct, UpdateLog: ulog})
req, _ := http.NewRequest("POST", b.cfg.Hammerhead+"/site/report", bytes.NewReader(body)) req, _ := http.NewRequest("POST", b.cfg.Hammerhead+"/site/report", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+b.cfg.Token) req.Header.Set("Authorization", "Bearer "+b.cfg.Token)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
@@ -414,3 +435,73 @@ func serveUDP(pc net.PacketConn) {
} }
} }
} }
// requestUpdate acts on "update now" from Hammerhead (id identifies the request). It only ever installs
// what this machine's own auto-update would (the newest release from its configured server), and only
// when auto-update is on. Caller holds b.mu.
func (b *Bamboo) requestUpdate(id string) {
if id == b.updateID {
return // already handled, or in progress
}
b.updateID, b.updateMsg = id, ""
b.updStage, b.updPct, b.updLog = "starting", 0, nil
b.updNote("Update requested from the dashboard")
if !b.cfg.AutoUpdate || b.cfg.UpdateNow == nil {
b.updateMsg = "auto-update is off on this machine (set BAMBOO_AUTO_UPDATE=1 and restart it)"
b.updStage = "failed"
b.updNote("Refused: " + b.updateMsg)
go b.report()
return
}
if !b.updating.CompareAndSwap(false, true) {
b.updateMsg = "another update is already running"
b.updStage = "failed"
b.updNote("Refused: " + b.updateMsg)
go b.report()
return
}
go func() {
defer b.updating.Store(false)
tag, err := b.cfg.UpdateNow(b.ctx, b.updProgress) // on success this restarts the process and does not return
b.mu.Lock()
switch {
case err != nil:
b.updateMsg = "update failed: " + err.Error()
case tag == "":
b.updateMsg = "already on the newest release"
default:
b.updateMsg = "updated to " + tag + ", restarting"
}
b.mu.Unlock()
b.report()
}()
}
// updNote adds a timestamped line to the update log. Caller holds b.mu.
func (b *Bamboo) updNote(line string) {
b.updLog = append(b.updLog, time.Now().Format("15:04:05")+" "+line)
if len(b.updLog) > 200 {
b.updLog = b.updLog[len(b.updLog)-200:]
}
}
// updProgress is called by the updater as it works; Hammerhead gets a fresh report at most once a
// second (and straight away for every new log line), so its progress bar follows along.
func (b *Bamboo) updProgress(stage string, pct int, line string) {
b.mu.Lock()
b.updStage = stage
if pct >= 0 {
b.updPct = pct
}
if line != "" {
b.updNote(line)
}
send := line != "" || time.Since(b.updSent) > time.Second
if send {
b.updSent = time.Now()
}
b.mu.Unlock()
if send {
go b.report()
}
}
+13 -1
View File
@@ -11,6 +11,7 @@ import (
"git.veillax.com/Veillax/bamboo" "git.veillax.com/Veillax/bamboo"
"git.veillax.com/Veillax/reef/dotenv" "git.veillax.com/Veillax/reef/dotenv"
"git.veillax.com/Veillax/reef/selfupdate"
) )
// version is set at build time (deploy/build.sh). // version is set at build time (deploy/build.sh).
@@ -34,6 +35,7 @@ func main() {
data := flag.String("data", env("BAMBOO_DATA", "./bamboo-data"), "directory for this agent's WireGuard key") data := flag.String("data", env("BAMBOO_DATA", "./bamboo-data"), "directory for this agent's WireGuard key")
dockerOn := flag.Bool("docker", os.Getenv("BAMBOO_DOCKER") == "1", "report local Docker containers so they can be picked as targets in the UI") dockerOn := flag.Bool("docker", os.Getenv("BAMBOO_DOCKER") == "1", "report local Docker containers so they can be picked as targets in the UI")
flag.String("env", ".env", "file to read secrets and settings from") flag.String("env", ".env", "file to read secrets and settings from")
autoUpdate := flag.Bool("auto-update", os.Getenv("BAMBOO_AUTO_UPDATE") == "1", "check for new releases every few hours and update and restart by itself (Linux and macOS)")
showVersion := flag.Bool("version", false, "print the version and exit") showVersion := flag.Bool("version", false, "print the version and exit")
flag.Parse() flag.Parse()
if *showVersion { if *showVersion {
@@ -47,7 +49,17 @@ func main() {
log.Printf("bamboo %s starting", version) log.Printf("bamboo %s starting", version)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop() defer stop()
b, err := bamboo.New(bamboo.Config{Hammerhead: strings.TrimRight(*hh, "/"), Token: *token, DataDir: *data, Docker: *dockerOn}) upd := selfupdate.Config{Server: "https://git.veillax.com", Repo: "Veillax/bamboo", App: "bamboo", Current: version, Logf: log.Printf}
if *autoUpdate {
go selfupdate.Run(ctx, upd)
}
b, err := bamboo.New(bamboo.Config{Hammerhead: strings.TrimRight(*hh, "/"), Token: *token, DataDir: *data, Docker: *dockerOn,
Version: version, AutoUpdate: *autoUpdate,
UpdateNow: func(ctx context.Context, progress func(string, int, string)) (string, error) {
c := upd
c.Progress = func(e selfupdate.Event) { progress(e.Stage, e.Pct, e.Line) }
return selfupdate.CheckOnce(ctx, c)
}})
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
+2 -2
View File
@@ -8,7 +8,7 @@ Wants=network-online.target
User=bamboo User=bamboo
Group=bamboo Group=bamboo
EnvironmentFile=-/etc/bamboo/bamboo.env EnvironmentFile=-/etc/bamboo/bamboo.env
ExecStart=/usr/local/bin/bamboo -data /var/lib/bamboo ExecStart=/usr/local/lib/bamboo/bamboo -data /var/lib/bamboo
WorkingDirectory=/var/lib/bamboo WorkingDirectory=/var/lib/bamboo
Restart=on-failure Restart=on-failure
RestartSec=3 RestartSec=3
@@ -16,7 +16,7 @@ RestartSec=3
# It makes outbound connections only, so it needs no privileges at all. # It makes outbound connections only, so it needs no privileges at all.
NoNewPrivileges=true NoNewPrivileges=true
ProtectSystem=strict ProtectSystem=strict
ReadWritePaths=/var/lib/bamboo ReadWritePaths=/var/lib/bamboo /usr/local/lib/bamboo
ProtectHome=true ProtectHome=true
PrivateTmp=true PrivateTmp=true
ProtectKernelTunables=true ProtectKernelTunables=true
+12 -6
View File
@@ -5,7 +5,7 @@
# sudo ./install.sh --start the same, then start (or restart) the service # sudo ./install.sh --start the same, then start (or restart) the service
# sudo ./install.sh --uninstall remove the service and binary (keeps the settings and data) # sudo ./install.sh --uninstall remove the service and binary (keeps the settings and data)
# #
# Files: /usr/local/bin/bamboo, /etc/systemd/system/bamboo.service, # Files: /usr/local/lib/bamboo/bamboo (linked from /usr/local/bin/bamboo), /etc/systemd/system/bamboo.service,
# /etc/bamboo/bamboo.env (your settings and secrets), /var/lib/bamboo (data). # /etc/bamboo/bamboo.env (your settings and secrets), /var/lib/bamboo (data).
# Set DESTDIR=/some/dir to stage everything there without touching the system. # Set DESTDIR=/some/dir to stage everything there without touching the system.
set -eu set -eu
@@ -17,6 +17,7 @@ UNIT=$ROOT/etc/systemd/system/$NAME.service
CONF=$ROOT/etc/$NAME CONF=$ROOT/etc/$NAME
ENVFILE=$CONF/$NAME.env ENVFILE=$CONF/$NAME.env
DATA=$ROOT/var/lib/$NAME DATA=$ROOT/var/lib/$NAME
LIB=$ROOT/usr/local/lib/$NAME # the program lives here, owned by the service, so auto-update can replace it
say() { printf '%s\n' "$*"; } say() { printf '%s\n' "$*"; }
real() { [ -z "${DESTDIR:-}" ]; } # are we touching the real system? real() { [ -z "${DESTDIR:-}" ]; } # are we touching the real system?
@@ -30,7 +31,7 @@ if [ "${1:-}" = "--uninstall" ]; then
if real; then if real; then
systemctl disable --now "$NAME" > /dev/null 2>&1 || true systemctl disable --now "$NAME" > /dev/null 2>&1 || true
fi fi
rm -f "$UNIT" "$BIN" rm -f "$UNIT" "$BIN" "$LIB/$NAME"
if real; then if real; then
systemctl daemon-reload || true systemctl daemon-reload || true
fi fi
@@ -46,15 +47,20 @@ if real && ! id "$NAME" > /dev/null 2>&1; then
fi fi
mkdir -p "$ROOT/usr/local/bin" "$ROOT/etc/systemd/system" "$CONF" "$DATA" mkdir -p "$ROOT/usr/local/bin" "$ROOT/etc/systemd/system" "$CONF" "$DATA"
install -m 0755 "$HERE/$NAME" "$BIN.new" mkdir -p "$LIB"
mv -f "$BIN.new" "$BIN" # atomic, so upgrading a running service is safe install -m 0755 "$HERE/$NAME" "$LIB/$NAME.new"
mv -f "$LIB/$NAME.new" "$LIB/$NAME" # atomic, so upgrading a running service is safe
rm -f "$BIN"
LINK=/usr/local/lib/$NAME/$NAME
[ -z "${DESTDIR:-}" ] || LINK=$LIB/$NAME # staging: point inside the staging folder
ln -s "$LINK" "$BIN"
install -m 0644 "$HERE/$NAME.service" "$UNIT" install -m 0644 "$HERE/$NAME.service" "$UNIT"
if [ ! -f "$ENVFILE" ]; then if [ ! -f "$ENVFILE" ]; then
install -m 0600 "$HERE/.env.example" "$ENVFILE" install -m 0600 "$HERE/.env.example" "$ENVFILE"
say "Created $ENVFILE. Edit it before starting." say "Created $ENVFILE. Edit it before starting."
fi fi
if real; then if real; then
chown -R "$NAME:$NAME" "$DATA" chown -R "$NAME:$NAME" "$DATA" "$LIB"
fi fi
if real; then if real; then
@@ -66,7 +72,7 @@ if real; then
fi fi
fi fi
say "Installed $("$BIN" -version 2> /dev/null || echo "$NAME")." say "Installed $("$LIB/$NAME" -version 2> /dev/null || echo "$NAME")."
say "Next:" say "Next:"
say " 1. edit $ENVFILE" say " 1. edit $ENVFILE"
say " 2. systemctl start $NAME (or run this again with --start)" say " 2. systemctl start $NAME (or run this again with --start)"
+18 -7
View File
@@ -42,19 +42,30 @@ else
echo "release $TAG already exists (id $id): replacing its files" echo "release $TAG already exists (id $id): replacing its files"
fi fi
existing=$(curl -fsS -H "$AUTH" "$API/releases/$id/assets" | json 'import sys, json # the ids of the release's assets with a given name (a failed upload can leave a partial one behind)
asset_ids() {
curl -fsS -H "$AUTH" "$API/releases/$id/assets" | "$PY" -c 'import sys, json
for a in json.load(sys.stdin): for a in json.load(sys.stdin):
print(a["id"], a["name"])') if a["name"] == sys.argv[1]:
print(a["id"])' "$1"
}
for f in dist/*.tar.gz dist/*.zip dist/SHA256SUMS; do for f in dist/*.tar.gz dist/*.zip dist/SHA256SUMS; do
[ -f "$f" ] || continue [ -f "$f" ] || continue
name=$(basename "$f") name=$(basename "$f")
echo "$existing" | while read -r aid aname; do n=1
if [ "$aname" = "$name" ]; then while :; do
for aid in $(asset_ids "$name"); do
curl -fsS -X DELETE -H "$AUTH" "$API/releases/$id/assets/$aid" > /dev/null curl -fsS -X DELETE -H "$AUTH" "$API/releases/$id/assets/$aid" > /dev/null
fi
done done
echo "uploading $name" echo "uploading $name (attempt $n)"
curl -fsS -X POST -H "$AUTH" -F "attachment=@$f" "$API/releases/$id/assets?name=$name" > /dev/null if curl -fsS --max-time 1200 -X POST -H "$AUTH" -F "attachment=@$f" "$API/releases/$id/assets?name=$name" > /dev/null; then
break
fi
# large uploads can hit a proxy timeout (HTTP 502); try again
[ "$n" -lt 5 ] || { echo "giving up on $name after $n attempts" >&2; exit 1; }
n=$((n + 1))
sleep $((n * 10))
done
done done
echo "published $SERVER/$REPO/releases/tag/$TAG" echo "published $SERVER/$REPO/releases/tag/$TAG"
+1 -1
View File
@@ -2,7 +2,7 @@ module git.veillax.com/Veillax/bamboo
go 1.27.3 go 1.27.3
require git.veillax.com/Veillax/reef v0.1.0 require git.veillax.com/Veillax/reef v0.1.7
require ( require (
github.com/Microsoft/go-winio v0.6.4 // indirect github.com/Microsoft/go-winio v0.6.4 // indirect
+8
View File
@@ -1,5 +1,13 @@
git.veillax.com/Veillax/reef v0.1.0 h1:O0OhEKFx2CsvACJ20sfzJNjGsngomtfDdKdxu4ROpCs= git.veillax.com/Veillax/reef v0.1.0 h1:O0OhEKFx2CsvACJ20sfzJNjGsngomtfDdKdxu4ROpCs=
git.veillax.com/Veillax/reef v0.1.0/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso= git.veillax.com/Veillax/reef v0.1.0/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso=
git.veillax.com/Veillax/reef v0.1.3 h1:RxSq2xwaup1+MieAkhlvOTZnYwPf19thFHEAe/7VxsI=
git.veillax.com/Veillax/reef v0.1.3/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso=
git.veillax.com/Veillax/reef v0.1.5 h1:BydjLJsfGgU4zEIqUsePJRjPi9NgbFdmvq0M8dvJCgM=
git.veillax.com/Veillax/reef v0.1.5/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso=
git.veillax.com/Veillax/reef v0.1.6 h1:NgZ4u2Z3hJ+BYD/zX9lov8fox4s2i8txtYNOpIsikVo=
git.veillax.com/Veillax/reef v0.1.6/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso=
git.veillax.com/Veillax/reef v0.1.7 h1:5C2udonlJac2xvDOE5e+zDxej+AbZ9ttrNSE4owmwuU=
git.veillax.com/Veillax/reef v0.1.7/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso=
github.com/Microsoft/go-winio v0.6.4 h1:kJF2pamtYCWXf0xAT8sAkwKC0pMl2HYBrRPHAqyUc+g= github.com/Microsoft/go-winio v0.6.4 h1:kJF2pamtYCWXf0xAT8sAkwKC0pMl2HYBrRPHAqyUc+g=
github.com/Microsoft/go-winio v0.6.4/go.mod h1:1xl5JXG4ze/m74GH6Dc4Mwxfc3VL5siRVQjIAlHH07U= github.com/Microsoft/go-winio v0.6.4/go.mod h1:1xl5JXG4ze/m74GH6Dc4Mwxfc3VL5siRVQjIAlHH07U=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=