server läuft
This commit is contained in:
1 parent
c8df9d7651
commit
42f0c0accb
21 files changed
+4185
-10
No files matched your search
+104
@@ -0,0 +1,104 @@
|
||||
// Package cli implementiert die Befehlszeile von sultan:
|
||||
//
|
||||
// sultan server Registry, Stream-Hub, Policy-Engine, Traefik-Provider
|
||||
// sultan agent (noch nicht implementiert)
|
||||
// sultan token (noch nicht implementiert)
|
||||
// sultan version
|
||||
//
|
||||
// Run ist ohne Prozessgrenzen testbar; main ruft nur Run auf.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"sultan/pkg/server"
|
||||
)
|
||||
|
||||
// Version wird beim Bauen gesetzt: -ldflags "-X sultan/pkg/cli.Version=1.2.3".
|
||||
var Version = "dev"
|
||||
|
||||
const usage = `sultan – verteiltes Ingress-Routing mit ZeroTrust-Service-Discovery
|
||||
|
||||
Befehle:
|
||||
sultan server Server starten (Registry, Agent-Port, Traefik-Provider)
|
||||
sultan agent Agent starten (noch nicht implementiert)
|
||||
sultan token Mandanten, Einladungen, Lizenzen verwalten (noch nicht implementiert)
|
||||
sultan version Version ausgeben
|
||||
sultan help diese Hilfe
|
||||
|
||||
Hilfe zu einem Befehl: sultan <befehl> --help
|
||||
`
|
||||
|
||||
// Run führt die Befehlszeile aus und liefert den Exit-Code (0 ok, 1 Fehler,
|
||||
// 2 Bedienfehler). ctx endet z. B. bei SIGTERM.
|
||||
func Run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprint(stderr, usage)
|
||||
return 2
|
||||
}
|
||||
switch args[0] {
|
||||
case "help", "-h", "--help":
|
||||
fmt.Fprint(stdout, usage)
|
||||
return 0
|
||||
case "version":
|
||||
fmt.Fprintf(stdout, "sultan %s\n", Version)
|
||||
return 0
|
||||
case "server":
|
||||
return runServer(ctx, args[1:], stdout, stderr)
|
||||
case "agent", "token":
|
||||
fmt.Fprintf(stderr, "sultan %s: noch nicht implementiert (siehe Roadmap)\n", args[0])
|
||||
return 1
|
||||
default:
|
||||
fmt.Fprintf(stderr, "unbekannter Befehl %q\n\n%s", args[0], usage)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
func runServer(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||
cfg, err := parseServerFlags(args, stderr)
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return 0
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
return 2
|
||||
}
|
||||
cfg.Logger = slog.New(slog.NewTextHandler(stderr, nil))
|
||||
s, err := server.New(ctx, cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintln(stderr, "Start fehlgeschlagen:", err)
|
||||
return 1
|
||||
}
|
||||
if err := s.Run(ctx); err != nil {
|
||||
fmt.Fprintln(stderr, "Server beendet mit Fehler:", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// parseServerFlags liest die Flags von `sultan server`; Defaults wie in
|
||||
// server.Config.
|
||||
func parseServerFlags(args []string, output io.Writer) (server.Config, error) {
|
||||
fs := flag.NewFlagSet("sultan server", flag.ContinueOnError)
|
||||
fs.SetOutput(output)
|
||||
var cfg server.Config
|
||||
fs.StringVar(&cfg.DataDir, "data-dir", "./suldata", "Datenverzeichnis (sultan.db, server.key/.crt, master.key)")
|
||||
fs.StringVar(&cfg.AgentAddr, "agent-addr", ":8443", "Adresse des Agent-Ports (nur TLS)")
|
||||
fs.StringVar(&cfg.ProviderAddr, "provider-addr", "127.0.0.1:8080",
|
||||
"Adresse für Traefik (/v1/traefik-config, Klartext – im Container :8080 und nur auf 127.0.0.1 veröffentlichen)")
|
||||
fs.StringVar(&cfg.InterceptorURL, "interceptor-url", "http://127.0.0.1:8081", "Adresse des Sultan-Interceptors/Landers")
|
||||
fs.DurationVar(&cfg.RefreshInterval, "refresh", time.Minute, "Intervall der Neubewertung aller Knoten")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return server.Config{}, err
|
||||
}
|
||||
if fs.NArg() > 0 {
|
||||
return server.Config{}, fmt.Errorf("unerwartetes Argument %q", fs.Arg(0))
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func run(ctx context.Context, args ...string) (code int, stdout, stderr string) {
|
||||
var out, errb bytes.Buffer
|
||||
code = Run(ctx, args, &out, &errb)
|
||||
return code, out.String(), errb.String()
|
||||
}
|
||||
|
||||
func TestRun_Usage(t *testing.T) {
|
||||
code, _, stderr := run(context.Background())
|
||||
if code != 2 || !strings.Contains(stderr, "sultan server") {
|
||||
t.Errorf("ohne Befehl: code=%d stderr=%q", code, stderr)
|
||||
}
|
||||
code, stdout, _ := run(context.Background(), "help")
|
||||
if code != 0 || !strings.Contains(stdout, "sultan server") || !strings.Contains(stdout, "sultan agent") {
|
||||
t.Errorf("help: code=%d stdout=%q", code, stdout)
|
||||
}
|
||||
code, _, stderr = run(context.Background(), "zaubern")
|
||||
if code != 2 || !strings.Contains(stderr, "zaubern") {
|
||||
t.Errorf("unbekannter Befehl: code=%d stderr=%q", code, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_Version(t *testing.T) {
|
||||
code, stdout, _ := run(context.Background(), "version")
|
||||
if code != 0 || strings.TrimSpace(stdout) != "sultan "+Version {
|
||||
t.Errorf("version: code=%d stdout=%q", code, stdout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_NotYetImplemented(t *testing.T) {
|
||||
for _, cmd := range []string{"agent", "token"} {
|
||||
code, _, stderr := run(context.Background(), cmd)
|
||||
if code != 1 || !strings.Contains(stderr, "noch nicht implementiert") {
|
||||
t.Errorf("%s: code=%d stderr=%q", cmd, code, stderr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerFlags(t *testing.T) {
|
||||
cfg, err := parseServerFlags(nil, &bytes.Buffer{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.DataDir != "./suldata" || cfg.AgentAddr != ":8443" || cfg.ProviderAddr != "127.0.0.1:8080" ||
|
||||
cfg.InterceptorURL != "http://127.0.0.1:8081" || cfg.RefreshInterval != time.Minute {
|
||||
t.Errorf("Defaults: %+v", cfg)
|
||||
}
|
||||
|
||||
cfg, err = parseServerFlags([]string{
|
||||
"--data-dir=/srv/sul", "--agent-addr", ":9443", "--provider-addr=:8080",
|
||||
"--interceptor-url=http://lander:8081", "--refresh=30s",
|
||||
}, &bytes.Buffer{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.DataDir != "/srv/sul" || cfg.AgentAddr != ":9443" || cfg.ProviderAddr != ":8080" ||
|
||||
cfg.InterceptorURL != "http://lander:8081" || cfg.RefreshInterval != 30*time.Second {
|
||||
t.Errorf("Overrides: %+v", cfg)
|
||||
}
|
||||
|
||||
if _, err := parseServerFlags([]string{"--gibts-nicht"}, &bytes.Buffer{}); err == nil {
|
||||
t.Error("unbekanntes Flag akzeptiert")
|
||||
}
|
||||
if _, err := parseServerFlags([]string{"überzählig"}, &bytes.Buffer{}); err == nil {
|
||||
t.Error("überzähliges Argument akzeptiert")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_ServerHelpAndBadFlags(t *testing.T) {
|
||||
code, stdout, stderr := run(context.Background(), "server", "--help")
|
||||
if code != 0 || !strings.Contains(stdout+stderr, "data-dir") {
|
||||
t.Errorf("server --help: code=%d out=%q err=%q", code, stdout, stderr)
|
||||
}
|
||||
code, _, _ = run(context.Background(), "server", "--bogus")
|
||||
if code != 2 {
|
||||
t.Errorf("server --bogus: code=%d", code)
|
||||
}
|
||||
}
|
||||
|
||||
// Startet den echten Server über die CLI und beendet ihn per Kontext
|
||||
// (wie SIGTERM in main).
|
||||
func TestRun_ServerStartsAndStops(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "suldata")
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
type result struct {
|
||||
code int
|
||||
stderr string
|
||||
}
|
||||
done := make(chan result, 1)
|
||||
go func() {
|
||||
code, _, stderr := run(ctx, "server", "--data-dir="+dir, "--agent-addr=127.0.0.1:0", "--provider-addr=127.0.0.1:0")
|
||||
done <- result{code, stderr}
|
||||
}()
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, "server.crt")); err == nil {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("Server hat das Datenverzeichnis nicht angelegt")
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case r := <-done:
|
||||
if r.code != 0 {
|
||||
t.Errorf("Exit-Code %d, stderr:\n%s", r.code, r.stderr)
|
||||
}
|
||||
if !strings.Contains(r.stderr, "Fingerprint") {
|
||||
t.Errorf("Start-Log ohne Fingerprint:\n%s", r.stderr)
|
||||
}
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("Server endet nicht")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_ServerStartError(t *testing.T) {
|
||||
file := filepath.Join(t.TempDir(), "datei")
|
||||
os.WriteFile(file, nil, 0o600)
|
||||
code, _, stderr := run(context.Background(), "server", "--data-dir="+filepath.Join(file, "sub"))
|
||||
if code != 1 || stderr == "" {
|
||||
t.Errorf("unbrauchbares Datenverzeichnis: code=%d stderr=%q", code, stderr)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user