trusted.pub now holds the release signing key, so self-upgrade, agent upgrade and node onboarding refuse a release whose checksums are not signed with it. Onboarding fetched the release on the node and compared it with a checksum from the same server; the control plane now fetches it for the node's architecture with release.Fetch, checks signature and checksum, and hands the bytes over. Release addresses must be https (loopback excepted). LLMGW_RELEASE_KEY overrides the key only in development builds. The CI release job fails instead of publishing an unsigned release while a key is built in. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
89 lines
2.6 KiB
Go
89 lines
2.6 KiB
Go
package agent
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
llmgwv1 "gitea.mynodes.xyz/ilker/llmgw/api/gen/llmgw/v1"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/release"
|
|
)
|
|
|
|
func TestAnAgentUpgradesItselfAndSaysSoBeforeRestarting(t *testing.T) {
|
|
body := []byte("the new agent")
|
|
sum := sha256.Sum256(body)
|
|
asset := release.AssetName("v9.9.9", runtime.GOOS, runtime.GOARCH)
|
|
// Signed, as every release is: with a key this test's build trusts.
|
|
pub, priv, kerr := release.GenerateKey()
|
|
if kerr != nil {
|
|
t.Fatal(kerr)
|
|
}
|
|
t.Setenv("LLMGW_RELEASE_KEY", pub)
|
|
sums := hex.EncodeToString(sum[:]) + " " + asset + "\n"
|
|
sig, _ := release.Sign(priv, []byte(sums))
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/v9.9.9/checksums.txt":
|
|
_, _ = w.Write([]byte(sums))
|
|
case "/v9.9.9/" + release.SignatureName:
|
|
_, _ = w.Write([]byte(sig))
|
|
case "/v9.9.9/" + asset:
|
|
_, _ = w.Write(body)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
bin := filepath.Join(t.TempDir(), "llmgw")
|
|
_ = os.WriteFile(bin, []byte("the old agent"), 0o755)
|
|
exited := make(chan struct{}, 1)
|
|
oldTarget, oldExit := upgradeTarget, exitForUpgrade
|
|
upgradeTarget = func() (string, error) { return bin, nil }
|
|
exitForUpgrade = func() { exited <- struct{}{} }
|
|
t.Cleanup(func() { upgradeTarget, exitForUpgrade = oldTarget, oldExit })
|
|
|
|
cfg := Config{Server: "127.0.0.1:7443", NodeID: "n1", WeightsCache: t.TempDir(), EnginesDir: t.TempDir()}
|
|
cfg.Superv.WorkDir = t.TempDir()
|
|
a, err := New(cfg, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := make(chan *llmgwv1.AgentMessage, 1)
|
|
go a.upgrade(t.Context(), &llmgwv1.UpgradeAgent{RequestId: "u1", Version: "v9.9.9", Releases: srv.URL}, out)
|
|
|
|
select {
|
|
case m := <-out:
|
|
if r := m.GetUpgrade(); r.GetRequestId() != "u1" || r.GetError() != "" {
|
|
t.Fatalf("reply %+v", r)
|
|
}
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("no reply")
|
|
}
|
|
select {
|
|
case <-exited:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("the agent did not restart into the new binary")
|
|
}
|
|
if b, _ := os.ReadFile(bin); string(b) != "the new agent" {
|
|
t.Errorf("binary holds %q", b)
|
|
}
|
|
|
|
// A version with no release answers with why, and does not restart.
|
|
go a.upgrade(t.Context(), &llmgwv1.UpgradeAgent{RequestId: "u2", Version: "v9.9.8", Releases: srv.URL}, out)
|
|
if r := (<-out).GetUpgrade(); r.GetError() == "" {
|
|
t.Error("an upgrade to a release that does not exist reported success")
|
|
}
|
|
select {
|
|
case <-exited:
|
|
t.Error("a failed upgrade restarted the agent")
|
|
case <-time.After(time.Second):
|
|
}
|
|
}
|