Every upgrade so far has been a script of mine copying a binary to each machine over ssh, restarting it and polling until it came back — which is the job, but not something the product could do, and which needed ssh into every node. `sudo llmgw ctl upgrade VERSION` on the control plane does it. This machine first: the release for its platform, checked against the release's checksums, the old binary kept beside it, llmgw-server restarted, and a wait until every node that was ready has reconnected. Then each node in turn: the control plane tells its agent the version over the stream it already holds; the agent fetches and checks the release itself, swaps it in with a rename, answers, and exits with status 75 so systemd starts the new binary. Engines keep running, as for any agent restart, and are adopted again. A node is done when it is back on the new version, ready, with as many ready replicas as before; one that is not within the timeout stops the run before the next node, so a bad release reaches one machine. Nodes already on the version are skipped, so running it again resumes. The fetch-and-verify is one package now, shared with onboarding's idea of what a release version looks like. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
62 lines
1.9 KiB
Go
62 lines
1.9 KiB
Go
package release
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func releaseServer(t *testing.T, version, asset string, body []byte, sum string) *httptest.Server {
|
|
t.Helper()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/" + version + "/checksums.txt":
|
|
_, _ = w.Write([]byte(sum + " " + asset + "\n"))
|
|
case "/" + version + "/" + asset:
|
|
_, _ = w.Write(body)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
|
|
func TestABinaryIsReplacedOnlyWithWhatTheReleasePublished(t *testing.T) {
|
|
dir := t.TempDir()
|
|
target := filepath.Join(dir, "llmgw")
|
|
_ = os.WriteFile(target, []byte("old"), 0o755)
|
|
body := []byte("new binary")
|
|
sum := sha256.Sum256(body)
|
|
asset := AssetName("v1.2.3", "linux", "amd64")
|
|
|
|
srv := releaseServer(t, "v1.2.3", asset, body, hex.EncodeToString(sum[:]))
|
|
backup, err := Install(t.Context(), srv.Client(), srv.URL, "v1.2.3", "linux", "amd64", target)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if b, _ := os.ReadFile(target); string(b) != "new binary" {
|
|
t.Errorf("target holds %q", b)
|
|
}
|
|
if b, _ := os.ReadFile(backup); string(b) != "old" {
|
|
t.Errorf("the old binary was not kept: %q", b)
|
|
}
|
|
|
|
// A download that does not match its checksum changes nothing.
|
|
bad := releaseServer(t, "v1.2.4", AssetName("v1.2.4", "linux", "amd64"), []byte("tampered"),
|
|
hex.EncodeToString(sum[:]))
|
|
if _, err := Install(t.Context(), bad.Client(), bad.URL, "v1.2.4", "linux", "amd64", target); err == nil {
|
|
t.Fatal("a binary not matching its checksum was installed")
|
|
}
|
|
if b, _ := os.ReadFile(target); string(b) != "new binary" {
|
|
t.Errorf("a failed install touched the binary: %q", b)
|
|
}
|
|
if _, err := Install(t.Context(), nil, srv.URL, "../../x", "linux", "amd64", target); err == nil {
|
|
t.Error("a version that is not one was fetched")
|
|
}
|
|
}
|