feat(cli): proxdeploy in Go
Befehle login, servers, distros, profiles, presets, vm, lxc, template list|build, batch, status, logs --follow, cancel. Renderer im Aufbau der Vorgabe (zeichengenau per Golden-File), 24-bit/256/ohne Farbe, --ascii, --quiet, --json, --dry-run; live im Terminal, zeilenweise ohne. SSE mit Wiederverbinden und Polling als Rückfall. Typen aus openapi.json per oapi-codegen; Release-Builds per cli/bauen.sh. Refs #013 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
package befehle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.wilde-server.de/florian.wilde/proxdeploy/cli/internal/api"
|
||||
)
|
||||
|
||||
func eineID(befehl string) cobra.PositionalArgs {
|
||||
return func(_ *cobra.Command, argumente []string) error {
|
||||
if len(argumente) != 1 {
|
||||
return bedienfehler("Auftrags-ID fehlt: proxdeploy %s <job-id>", befehl)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u *umgebung) statusBefehl() *cobra.Command {
|
||||
var folgen bool
|
||||
befehl := &cobra.Command{
|
||||
Use: "status <job-id>",
|
||||
Short: "Stand eines Auftrags (mit --follow live bis zum Ende)",
|
||||
Args: eineID("status"),
|
||||
RunE: func(cmd *cobra.Command, argumente []string) error {
|
||||
c, _, err := u.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if folgen {
|
||||
_, err := u.folgen(cmd.Context(), c, argumente[0])
|
||||
return err
|
||||
}
|
||||
a, err := c.Auftrag(cmd.Context(), argumente[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fertig := endzustaende[a.Status]
|
||||
if u.opt.json {
|
||||
if err := u.jsonAusgeben(a); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
d := u.neuerDarsteller()
|
||||
d.live = true // einmal vollständig zeichnen, ohne Neuzeichnen
|
||||
d.zeichnen(a, fertig)
|
||||
}
|
||||
if fertig && a.Status != api.AuftragStatusERFOLGREICH {
|
||||
return &exitFehler{code: ExitFehlschlag, gemeldet: true}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
befehl.Flags().BoolVarP(&folgen, "follow", "f", false, "live verfolgen bis zum Ende")
|
||||
return befehl
|
||||
}
|
||||
|
||||
func (u *umgebung) logsBefehl() *cobra.Command {
|
||||
var folgen bool
|
||||
befehl := &cobra.Command{
|
||||
Use: "logs <job-id>",
|
||||
Short: "Detail-Log eines Auftrags (mit --follow live)",
|
||||
Args: eineID("logs"),
|
||||
RunE: func(cmd *cobra.Command, argumente []string) error {
|
||||
c, _, err := u.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id := argumente[0]
|
||||
zeilen, err := c.Log(cmd.Context(), id, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, z := range zeilen {
|
||||
u.logZeile(z)
|
||||
}
|
||||
if !folgen {
|
||||
return nil
|
||||
}
|
||||
letzte := ""
|
||||
if len(zeilen) > 0 {
|
||||
letzte = zeilen[len(zeilen)-1].Id
|
||||
}
|
||||
return u.logFolgen(cmd.Context(), c, id, letzte)
|
||||
},
|
||||
}
|
||||
befehl.Flags().BoolVarP(&folgen, "follow", "f", false, "neue Zeilen live anhängen, bis der Auftrag fertig ist")
|
||||
return befehl
|
||||
}
|
||||
|
||||
func (u *umgebung) logZeile(z api.LogZeile) {
|
||||
if u.opt.json {
|
||||
daten, _ := json.Marshal(z)
|
||||
fmt.Fprintln(u.aus, string(daten))
|
||||
return
|
||||
}
|
||||
s := u.stil()
|
||||
zeit := z.Zeit
|
||||
if t, err := time.Parse(time.RFC3339Nano, z.Zeit); err == nil {
|
||||
zeit = t.Local().Format("15:04:05")
|
||||
}
|
||||
phase := ""
|
||||
if z.Phase != nil {
|
||||
phase = *z.Phase
|
||||
}
|
||||
nachricht := z.Nachricht
|
||||
switch z.Stufe {
|
||||
case "error":
|
||||
nachricht = s.Rot(nachricht)
|
||||
case "warn":
|
||||
nachricht = s.Amber(nachricht)
|
||||
default:
|
||||
nachricht = s.Paper(nachricht)
|
||||
}
|
||||
fmt.Fprintf(u.aus, "%s %s %s\n", s.Muted(zeit), s.Muted(fmt.Sprintf("%-16s", phase)), nachricht)
|
||||
}
|
||||
|
||||
// logFolgen hängt neue Zeilen an (SSE-Ereignis `log`), bis der Auftrag fertig ist.
|
||||
func (u *umgebung) logFolgen(ctx context.Context, c *api.Client, id, ab string) error {
|
||||
for versuch := 0; versuch < 5; versuch++ {
|
||||
err := c.Ereignisse(ctx, id, ab, func(e api.Ereignis) error {
|
||||
switch e.Art {
|
||||
case "log":
|
||||
var zeilen []api.LogZeile
|
||||
if err := json.Unmarshal(e.Daten, &zeilen); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, z := range zeilen {
|
||||
u.logZeile(z)
|
||||
ab = z.Id
|
||||
}
|
||||
case "ende":
|
||||
return api.Ende
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if errors.Is(err, api.Ende) || ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
var apiFehler *api.Fehler
|
||||
if errors.As(err, &apiFehler) {
|
||||
return err
|
||||
}
|
||||
time.Sleep(time.Duration(versuch+1) * time.Second)
|
||||
}
|
||||
return &api.VerbindungsFehler{Ursache: errors.New("Ereignisstrom bricht wiederholt ab")}
|
||||
}
|
||||
|
||||
func (u *umgebung) cancelBefehl() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "cancel <job-id>",
|
||||
Short: "Auftrag abbrechen (hält an der nächsten Phasengrenze)",
|
||||
Args: eineID("cancel"),
|
||||
RunE: func(cmd *cobra.Command, argumente []string) error {
|
||||
c, _, err := u.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.Abbrechen(cmd.Context(), argumente[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
if !u.opt.json {
|
||||
fmt.Fprintln(u.aus, u.stil().Amber("Abbruch angefordert – "+strings.TrimSpace(argumente[0])+" hält an der nächsten Phasengrenze."))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user