- serve prints a startup summary on a terminal (version, environment, URL, admin, database host and name, pending migrations, plugins, drivers) and logs it as a 'startup summary' record plus 'listening on <addr>' - on SIGINT/SIGTERM serve drains, prints and logs a shutdown report and returns nil (exit 0), or the drain errors (exit 1) - the request log is on by default under diary component http with request_id; the route pattern, never the path, is logged - lagoon.PendingMigrations, conga.Worker.Stats, diary.HideOnTTY, bonfire.IsTerminal and bonfire.Details back the summary and report
207 lines
6.0 KiB
Go
207 lines
6.0 KiB
Go
package bonfire
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
)
|
|
|
|
// consoleState is the optional side of an Output that knows the running
|
|
// command's global options. The bonfire console implements it; other
|
|
// Output implementations give the zero values.
|
|
type consoleState interface {
|
|
consoleGlobals() Globals
|
|
consoleInteractive() bool
|
|
consoleTerminal() bool
|
|
}
|
|
|
|
func (c *console) consoleGlobals() Globals { return c.globals }
|
|
|
|
func (c *console) consoleInteractive() bool { return c.canPrompt() }
|
|
|
|
func (c *console) consoleTerminal() bool { return c.tty }
|
|
|
|
// GlobalsOf returns the global options of the command out belongs to, or
|
|
// the zero Globals when out is not a bonfire console.
|
|
func GlobalsOf(out Output) Globals {
|
|
if s, ok := out.(consoleState); ok {
|
|
return s.consoleGlobals()
|
|
}
|
|
return Globals{}
|
|
}
|
|
|
|
// VerbosityOf returns the -v level (0 to 3) of the command out belongs to.
|
|
func VerbosityOf(out Output) int {
|
|
return GlobalsOf(out).Verbosity
|
|
}
|
|
|
|
// IsQuiet reports whether the command runs with -q.
|
|
func IsQuiet(out Output) bool {
|
|
return GlobalsOf(out).Quiet
|
|
}
|
|
|
|
// Interactive reports whether out can ask a person: stdin and stdout are
|
|
// terminals and neither -n nor -q is set. It is false for other Output
|
|
// implementations and inside bonfire.Call.
|
|
func Interactive(out Output) bool {
|
|
if s, ok := out.(consoleState); ok {
|
|
return s.consoleInteractive()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsTerminal reports whether out writes to a terminal (its stdout is one).
|
|
// It is false for other Output implementations and inside bonfire.Call.
|
|
// Long-running commands use it to choose between a styled summary block for
|
|
// a person and log records for journald.
|
|
func IsTerminal(out Output) bool {
|
|
if s, ok := out.(consoleState); ok {
|
|
return s.consoleTerminal()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Detail is one line of a Details block: a label and its value. Warn
|
|
// prints the value in the warning color.
|
|
type Detail struct {
|
|
Label string
|
|
Value string
|
|
Warn bool
|
|
}
|
|
|
|
// Details prints title (when not empty) and rows as aligned label/value
|
|
// lines in the console palette: muted labels padded to the longest one,
|
|
// values in the default color or, with Warn, in the warning color. It
|
|
// prints nothing under -q. serve, queue:work and summer dev use it for
|
|
// their startup summaries and shutdown reports.
|
|
func Details(out Output, title string, rows []Detail) {
|
|
if IsQuiet(out) {
|
|
return
|
|
}
|
|
width := 0
|
|
for _, r := range rows {
|
|
width = max(width, len([]rune(r.Label)))
|
|
}
|
|
c, styled := out.(*console)
|
|
var b strings.Builder
|
|
if title != "" {
|
|
if styled {
|
|
b.WriteString(c.palette.Header.Render(title))
|
|
} else {
|
|
b.WriteString(title)
|
|
}
|
|
b.WriteByte('\n')
|
|
}
|
|
for _, r := range rows {
|
|
label := r.Label + strings.Repeat(" ", width-len([]rune(r.Label)))
|
|
value := r.Value
|
|
if styled {
|
|
label = c.palette.Muted.Render(label)
|
|
if r.Warn {
|
|
value = c.palette.Warning.Render(value)
|
|
}
|
|
}
|
|
b.WriteString(" " + label + " " + value + "\n")
|
|
}
|
|
if styled {
|
|
c.emit(c.out, b.String())
|
|
return
|
|
}
|
|
out.Printf("%s", b.String())
|
|
}
|
|
|
|
// ConfirmProceed asks question before a destructive step, defaulting to
|
|
// no, only when out is Interactive. Off a terminal, with -n or -q and in
|
|
// bonfire.Call it returns true without asking, so deploys over SSH and
|
|
// scheduled runs never block (D-28).
|
|
func ConfirmProceed(out Output, question string) (bool, error) {
|
|
if !Interactive(out) {
|
|
return true, nil
|
|
}
|
|
return out.Confirm(question, false)
|
|
}
|
|
|
|
// Current tells p which item is being worked on, when p is an ItemProgress.
|
|
func Current(p Progress, item string) {
|
|
if ip, ok := p.(ItemProgress); ok {
|
|
ip.Current(item)
|
|
}
|
|
}
|
|
|
|
// Failed records a failed item on p, when p is an ItemProgress.
|
|
func Failed(p Progress, item string, err error) {
|
|
if ip, ok := p.(ItemProgress); ok {
|
|
ip.Fail(item, err)
|
|
}
|
|
}
|
|
|
|
// Process runs fn for each item in turn. On a live console it shows SSU's
|
|
// process view: one row per item (waiting, running, done or failed with its
|
|
// error) and a progress bar labelled operation. Otherwise it prints one line
|
|
// per item, "✓ item" through Output.Success or "✗ item: error" through
|
|
// Output.Error, so -q prints only the failures. It stops early when ctx is
|
|
// cancelled and returns errors.Join of the failures.
|
|
func Process(ctx context.Context, out Output, operation string, items []string, fn func(ctx context.Context, item string) error) error {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
if c, ok := out.(*console); ok && c.live() && len(items) > 0 {
|
|
var errs []error
|
|
_ = runLive(ctx, c.out, c, newProcessModel(c.palette, operation, items), func(send func(tea.Msg)) error {
|
|
for i, item := range items {
|
|
if err := ctx.Err(); err != nil {
|
|
errs = append(errs, err)
|
|
return nil
|
|
}
|
|
send(stepMsg{item: item, state: itemRunning, done: i})
|
|
if err := fn(ctx, item); err != nil {
|
|
errs = append(errs, failure(item, err))
|
|
send(stepMsg{item: item, state: itemFailed, err: err, done: i + 1})
|
|
continue
|
|
}
|
|
send(stepMsg{item: item, state: itemDone, done: i + 1})
|
|
}
|
|
return nil
|
|
})
|
|
return errors.Join(errs...)
|
|
}
|
|
var errs []error
|
|
for _, item := range items {
|
|
if err := ctx.Err(); err != nil {
|
|
errs = append(errs, err)
|
|
break
|
|
}
|
|
if err := fn(ctx, item); err != nil {
|
|
errs = append(errs, failure(item, err))
|
|
out.Error(failureLine(item, err))
|
|
continue
|
|
}
|
|
out.Success(item)
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
// failure wraps err with the item it belongs to.
|
|
func failure(item string, err error) error {
|
|
return &itemError{item: item, err: err}
|
|
}
|
|
|
|
type itemError struct {
|
|
item string
|
|
err error
|
|
}
|
|
|
|
func (e *itemError) Error() string { return failureLine(e.item, e.err) }
|
|
|
|
func (e *itemError) Unwrap() error { return e.err }
|
|
|
|
// PrintAbove prints line above the running live view (spinner, progress or
|
|
// process view) and reports whether one was running. diary's console
|
|
// channel uses it so log lines from other goroutines never tear a view; it
|
|
// writes directly when PrintAbove returns false.
|
|
func PrintAbove(line string) bool {
|
|
return printAboveLines(line)
|
|
}
|