feat: CLI-Skeleton mit version/serve/check und Makefile

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
This commit is contained in:
Carsten Abele 2026-08-14 09:03:31 +02:00
parent bb84607bf8
commit fbfff90b38
8 changed files with 196 additions and 0 deletions

View file

@ -0,0 +1,14 @@
package main
import "fmt"
// Per -ldflags gesetzt; siehe Makefile.
var (
version = "dev"
commit = "none"
date = "unknown"
)
func versionString() string {
return fmt.Sprintf("vpnportal %s (commit %s, built %s)", version, commit, date)
}

17
cmd/vpnportal/check.go Normal file
View file

@ -0,0 +1,17 @@
package main
import (
"fmt"
"io"
)
func runCheck(args []string, stdout, stderr io.Writer) int {
fs, path := configFlags("check", stderr)
testAuth := fs.String("test-auth", "", "Suche und Gruppenprüfung für einen Benutzer durchspielen")
if err := fs.Parse(args); err != nil {
return 2
}
_ = testAuth
fmt.Fprintf(stderr, "Fehler: config %q kann noch nicht geladen werden (nicht implementiert)\n", *path)
return 1
}

46
cmd/vpnportal/main.go Normal file
View file

@ -0,0 +1,46 @@
package main
import (
"fmt"
"io"
"os"
)
const usage = `vpnportal VPN-Konfig-Portal für OPNsense
Aufruf:
vpnportal serve [--config PFAD] Portal starten (Default)
vpnportal check [--config PFAD] [--test-auth BENUTZER]
vpnportal version
Optionen:
--config PFAD Pfad zur Konfigurationsdatei (Default: /etc/vpnportal/config.yaml)
`
func main() {
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
}
func run(args []string, stdout, stderr io.Writer) int {
cmd := "serve"
rest := args
if len(args) > 0 && len(args[0]) > 0 && args[0][0] != '-' {
cmd, rest = args[0], args[1:]
}
switch cmd {
case "version":
fmt.Fprintln(stdout, versionString())
return 0
case "serve":
return runServe(rest, stdout, stderr)
case "check":
return runCheck(rest, stdout, stderr)
case "help", "-h", "--help":
fmt.Fprint(stdout, usage)
return 0
default:
fmt.Fprintf(stderr, "Unbekanntes Kommando: %s\n\n%s", cmd, usage)
return 2
}
}

View file

@ -0,0 +1,43 @@
package main
import (
"bytes"
"strings"
"testing"
)
func TestRunVersion(t *testing.T) {
var out, errOut bytes.Buffer
code := run([]string{"version"}, &out, &errOut)
if code != 0 {
t.Fatalf("exit code = %d, want 0 (stderr: %s)", code, errOut.String())
}
got := out.String()
for _, want := range []string{"vpnportal", "commit", "built"} {
if !strings.Contains(got, want) {
t.Errorf("output %q missing %q", got, want)
}
}
}
func TestRunUnknownCommand(t *testing.T) {
var out, errOut bytes.Buffer
if code := run([]string{"frobnicate"}, &out, &errOut); code != 2 {
t.Fatalf("exit code = %d, want 2", code)
}
if !strings.Contains(errOut.String(), "frobnicate") {
t.Errorf("stderr should name the unknown command, got %q", errOut.String())
}
}
func TestRunNoArgsDefaultsToServe(t *testing.T) {
var out, errOut bytes.Buffer
// serve ohne --config muss mit Fehler abbrechen, nicht mit "unknown command"
code := run(nil, &out, &errOut)
if code == 2 {
t.Fatalf("no args must default to serve, not unknown-command")
}
if !strings.Contains(errOut.String(), "config") {
t.Errorf("stderr should complain about missing config, got %q", errOut.String())
}
}

25
cmd/vpnportal/serve.go Normal file
View file

@ -0,0 +1,25 @@
package main
import (
"flag"
"fmt"
"io"
)
const defaultConfigPath = "/etc/vpnportal/config.yaml"
func configFlags(name string, stderr io.Writer) (*flag.FlagSet, *string) {
fs := flag.NewFlagSet(name, flag.ContinueOnError)
fs.SetOutput(stderr)
path := fs.String("config", defaultConfigPath, "Pfad zur Konfigurationsdatei")
return fs, path
}
func runServe(args []string, stdout, stderr io.Writer) int {
fs, path := configFlags("serve", stderr)
if err := fs.Parse(args); err != nil {
return 2
}
fmt.Fprintf(stderr, "Fehler: config %q kann noch nicht geladen werden (nicht implementiert)\n", *path)
return 1
}