Everything in server.yaml that is not one machine's own is now a versioned document in the store (cluster_settings), the same on every control plane. The first start against a store imports the file's cluster keys, with the baseline guard policy when there is none; after that those keys in a file are ignored, with a warning. Each control plane polls the document's version and its own server.yaml's hash. A change is validated and applied: guard live (Gate.Replace), anything else by a graceful restart that systemd brings back, standbys first and the leader 30s later. A version that does not load on a machine is not applied; at start the newest one that loads is used, so a bad edit cannot keep every control plane down. llmgw ctl config show/get/set/unset/edit/history/rollback, the admin API under /api/v1/settings, and the console's Settings screen, which layers the store's document over the machine's file, write it. Commands beside the control plane that read cluster keys (init, node add, server add, backup, upgrade) read the layered configuration; init --bundle writes offline to the store once it holds the settings. A standby that becomes the leader re-reads models, engine choices and facility holds from the store. Retires appendGuardIfMissing: the import does it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
175 lines
5.1 KiB
Go
175 lines
5.1 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/config"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
|
|
)
|
|
|
|
func newTestInit(t *testing.T) (*serverInit, *bytes.Buffer) {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
var out bytes.Buffer
|
|
return &serverInit{
|
|
etcDir: filepath.Join(root, "etc"),
|
|
stateDir: filepath.Join(root, "state"),
|
|
pkiDir: filepath.Join(root, "etc", "pki"),
|
|
admin: "admin@example.com",
|
|
out: &out,
|
|
}, &out
|
|
}
|
|
|
|
func TestInitLeavesAControlPlaneThatStarts(t *testing.T) {
|
|
in, out := newTestInit(t)
|
|
if err := in.run(context.Background()); err != nil {
|
|
t.Fatalf("%v\n%s", err, out)
|
|
}
|
|
|
|
cfg, err := config.Load(in.configPath())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The configuration install.md used to show had an empty catalog and no
|
|
// control.enabled, which the server refuses as having nothing to serve.
|
|
if !cfg.ControlPlaneMode() {
|
|
t.Error("the written configuration does not run a control plane")
|
|
}
|
|
if !cfg.AgentTLS.Enabled() || !cfg.Auth.Enabled {
|
|
t.Error("mutual TLS and authentication should both be on")
|
|
}
|
|
for _, f := range []string{"ca.pem", "server.pem", "server-key.pem"} {
|
|
if _, err := os.Stat(filepath.Join(in.etcDir, f)); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
s, err := store.Open(context.Background(), cfg.Auth.Store)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
u, err := s.UserByEmail(context.Background(), "admin@example.com")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
inDefault, _ := s.CanUseProject(context.Background(), u.ID, "default")
|
|
if !u.IsAdmin || u.State != store.UserActive || !inDefault {
|
|
t.Errorf("administrator not ready to use: %+v", u)
|
|
}
|
|
if !strings.Contains(out.String(), "password: ") {
|
|
t.Error("the generated password was not shown")
|
|
}
|
|
}
|
|
|
|
func TestInitAgainChangesNothing(t *testing.T) {
|
|
in, out := newTestInit(t)
|
|
if err := in.run(context.Background()); err != nil {
|
|
t.Fatalf("%v\n%s", err, out)
|
|
}
|
|
cert, _ := os.ReadFile(filepath.Join(in.etcDir, "server.pem"))
|
|
|
|
again := *in
|
|
again.changed = false
|
|
out.Reset()
|
|
if err := again.run(context.Background()); err != nil {
|
|
t.Fatalf("%v\n%s", err, out)
|
|
}
|
|
if again.changed {
|
|
t.Errorf("a second run changed something:\n%s", out)
|
|
}
|
|
if strings.Contains(out.String(), "password: ") {
|
|
t.Error("a second run must not reset the administrator's password")
|
|
}
|
|
if now, _ := os.ReadFile(filepath.Join(in.etcDir, "server.pem")); !bytes.Equal(now, cert) {
|
|
t.Error("the certificate was reissued without reason")
|
|
}
|
|
}
|
|
|
|
func TestInitWithANewHostKeepsTheOldOnes(t *testing.T) {
|
|
in, out := newTestInit(t)
|
|
in.hosts = []string{"old.internal"}
|
|
if err := in.run(context.Background()); err != nil {
|
|
t.Fatalf("%v\n%s", err, out)
|
|
}
|
|
in.hosts = []string{"new.internal"}
|
|
if err := in.run(context.Background()); err != nil {
|
|
t.Fatalf("%v\n%s", err, out)
|
|
}
|
|
cert, err := readCert(filepath.Join(in.etcDir, "server.pem"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, h := range []string{"old.internal", "new.internal"} {
|
|
if !slices.Contains(cert.DNSNames, h) {
|
|
t.Errorf("certificate lost %s: %v", h, cert.DNSNames)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInitNeverOverwritesTheConfiguration(t *testing.T) {
|
|
in, out := newTestInit(t)
|
|
if err := os.MkdirAll(in.etcDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mine := "# mine\ncontrol:\n enabled: true\nauth:\n enabled: true\n store: " +
|
|
filepath.Join(in.stateDir, "mine.db") + "\n"
|
|
if err := os.WriteFile(in.configPath(), []byte(mine), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := in.run(context.Background()); err != nil {
|
|
t.Fatalf("%v\n%s", err, out)
|
|
}
|
|
if got, _ := os.ReadFile(in.configPath()); string(got) != mine {
|
|
t.Error("an existing server.yaml was rewritten")
|
|
}
|
|
// And the store it names is the one used.
|
|
if _, err := os.Stat(filepath.Join(in.stateDir, "mine.db")); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestTheInitUnitIsTheShippedUnit(t *testing.T) {
|
|
b, err := os.ReadFile("../../deploy/systemd/llmgw-server.service")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(b) != serverUnit {
|
|
t.Error("deploy/systemd/llmgw-server.service and the unit 'server init' writes have drifted")
|
|
}
|
|
}
|
|
|
|
func TestInitKeepsCertificatesFromAnAuthorityElsewhere(t *testing.T) {
|
|
in, out := newTestInit(t)
|
|
// Issued on another machine by an authority that never comes here.
|
|
elsewhere := &serverInit{etcDir: in.etcDir, pkiDir: filepath.Join(t.TempDir(), "pki"), out: &bytes.Buffer{}}
|
|
if err := os.MkdirAll(in.etcDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := elsewhere.ensureCerts(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before, _ := os.ReadFile(filepath.Join(in.etcDir, "server.pem"))
|
|
|
|
if err := in.run(context.Background()); err != nil {
|
|
t.Fatalf("%v\n%s", err, out)
|
|
}
|
|
if after, _ := os.ReadFile(filepath.Join(in.etcDir, "server.pem")); !bytes.Equal(before, after) {
|
|
t.Error("a certificate from another authority was replaced")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(in.pkiDir, "ca-key.pem")); err == nil {
|
|
t.Error("an authority was created beside one that already exists elsewhere")
|
|
}
|
|
|
|
in.hosts = []string{"new.internal"}
|
|
if err := in.run(context.Background()); err == nil {
|
|
t.Error("--host cannot be honoured without the authority, and should say so")
|
|
}
|
|
}
|