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>
175 lines
4.2 KiB
Go
175 lines
4.2 KiB
Go
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
|
||
},
|
||
}
|
||
}
|