render.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. package commands
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "io"
  7. )
  8. // ErrNoTextRendering is returned by Render when a command's Value has no human rendering
  9. // and text was asked for anyway. A command declares this by leaving Text nil and saying
  10. // so with JSONOnly; it is not a fallback situation but a statement that no such form
  11. // exists yet, so nobody builds a script around an interim one. Each surface's default
  12. // policy avoids ever asking.
  13. var ErrNoTextRendering = errors.New("no human readable output yet; pass -json or -pretty")
  14. // Format is how a result should be presented.
  15. type Format int
  16. const (
  17. // FormatUnset is a request that expressed no preference. A surface resolves it with
  18. // its own default before rendering; Render itself treats it as FormatText.
  19. FormatUnset Format = iota
  20. // FormatText renders for a person. It is what an operator gets by default on every
  21. // surface with a human at the other end.
  22. FormatText
  23. // FormatJSON renders the result as compact JSON.
  24. FormatJSON
  25. // FormatPrettyJSON renders the result as indented JSON.
  26. FormatPrettyJSON
  27. )
  28. // Flag names for the two presentation flags. They are offered on every command rather
  29. // than declared by each one, because how a result is displayed is not a property of the
  30. // command that produced it.
  31. const (
  32. FlagJSON = "json"
  33. FlagPretty = "pretty"
  34. )
  35. // Flag names nebula ctl takes for itself, anywhere on its line: -config and -socket say
  36. // which daemon to talk to, and the client picks them out before it knows which command
  37. // the line names, let alone that command's flags. A command that declared either would
  38. // never receive it over that surface, so both are reserved.
  39. const (
  40. FlagConfig = "config"
  41. FlagSocket = "socket"
  42. )
  43. // reservedFlags are handled before a command's Run is called, so a command may not
  44. // declare them itself. -h and -help are intercepted as a request for usage; -json and
  45. // -pretty are added to every command's FlagSet by commandFlags, on every surface, so a
  46. // command that defined either would make that definition panic with "flag redefined"
  47. // on whichever goroutine ran the command; and -config and -socket are nebula ctl's own,
  48. // taken off the line before the command is parsed.
  49. var reservedFlags = []string{"h", "help", FlagJSON, FlagPretty, FlagConfig, FlagSocket}
  50. // Render writes r in the requested format.
  51. //
  52. // This is the function ADR 004 means by "the same rendering functions": every surface
  53. // calls it, so a hostmap looks the same over ssh as it does through the control socket,
  54. // and a change to either rendering happens once.
  55. func Render(c *Command, r Result, f Format, w io.Writer) error {
  56. switch f {
  57. case FormatJSON:
  58. return renderJSON(r.Payload(), w, false)
  59. case FormatPrettyJSON:
  60. return renderJSON(r.Payload(), w, true)
  61. default:
  62. // An outcome reads the same for every command: its reason, as a sentence. Only a
  63. // Value needs the command's own rendering, so Text never sees anything else.
  64. if r.Value == nil {
  65. _, err := io.WriteString(w, r.Reason+"\n")
  66. return err
  67. }
  68. if c != nil && c.Text != nil {
  69. return c.Text(r.Value, w)
  70. }
  71. name := "this command"
  72. if c != nil {
  73. name = string(c.Name)
  74. }
  75. return fmt.Errorf("%s has %w", name, ErrNoTextRendering)
  76. }
  77. }
  78. func renderJSON(v any, w io.Writer, indent bool) error {
  79. enc := json.NewEncoder(w)
  80. if indent {
  81. enc.SetIndent("", " ")
  82. }
  83. return enc.Encode(v)
  84. }
  85. // Result is what every Run returns: whether the command worked, a sentence saying so,
  86. // and, for a command that reports rather than acts, the value it produced.
  87. //
  88. // Value and Reason are alternatives. A command that reports returns Value(v), and the
  89. // surfaces render v, as JSON or through the command's Text. A command that acts, or any
  90. // command refusing its arguments, returns Did or Failed, and the surfaces render the
  91. // Outcome: its Reason is the sentence these commands have always printed. Nothing is for
  92. // a command with nothing to say.
  93. //
  94. // The envelope itself never reaches a caller. Payload is what JSON carries, and Render
  95. // hands Text only the Value, so a command's renderer sees the one type it returns on
  96. // success and is never asked to recognise an Outcome.
  97. type Result struct {
  98. Outcome
  99. Value any
  100. // NotFound marks a refusal that is about the thing asked for rather than the asking:
  101. // a vpn address with no tunnel behind it, as opposed to one that would not parse. It
  102. // renders exactly as any other refusal, and never reaches the wire; the distinction
  103. // is for a surface whose protocol has a word for it, so an HTTP client is told 404
  104. // rather than 400 and never has to parse the sentence.
  105. NotFound bool
  106. }
  107. // Outcome is the part of a Result that says whether the command worked and, in words a
  108. // caller can show someone, why. It is what a program receives from a command that acts
  109. // rather than reports: closing a tunnel, changing a remote, reloading config.
  110. //
  111. // One shape serves all of them. Designing a payload per outcome would be work without a
  112. // reader. Reason is set either way, so a success says what it did rather than leaving
  113. // the caller to infer it from a bare true.
  114. type Outcome struct {
  115. OK bool `json:"ok"`
  116. Reason string `json:"reason"`
  117. }
  118. // Value is the Result of a command that reports: v is what it has to show, and it must
  119. // marshal to JSON.
  120. func Value(v any) Result {
  121. return Result{Outcome: Outcome{OK: true}, Value: v}
  122. }
  123. // Failed is a convenience for the common "it did not work, here is why" case.
  124. func Failed(format string, args ...any) Result {
  125. return Result{Outcome: Outcome{Reason: fmt.Sprintf(format, args...)}}
  126. }
  127. // NotFound is Failed for a command asked about something that does not exist. See
  128. // Result.NotFound.
  129. func NotFound(format string, args ...any) Result {
  130. r := Failed(format, args...)
  131. r.NotFound = true
  132. return r
  133. }
  134. // Did is a convenience for the common "it worked, here is what happened" case.
  135. func Did(format string, args ...any) Result {
  136. return Result{Outcome: Outcome{OK: true, Reason: fmt.Sprintf(format, args...)}}
  137. }
  138. // Nothing is the Result of a command with nothing to say: it did its work, and the
  139. // surface sends an empty, successful reply without rendering anything.
  140. func Nothing() Result {
  141. return Result{Outcome: Outcome{OK: true}}
  142. }
  143. // Empty reports a Result with nothing to render: no Value and no Reason. The zero Result
  144. // counts, so a Run that returns Result{} says nothing rather than failing silently.
  145. func (r Result) Empty() bool {
  146. return r.Value == nil && r.Reason == ""
  147. }
  148. // Payload is what a program receives: the Value when there is one, otherwise the
  149. // Outcome. Neither surface ever sends the envelope itself, so print-tunnel -json emits
  150. // the tunnel and close-tunnel -json emits {ok, reason}, as both always have.
  151. func (r Result) Payload() any {
  152. if r.Value != nil {
  153. return r.Value
  154. }
  155. return r.Outcome
  156. }
  157. // Typed adapts a renderer for one result type to the TextFunc a Command declares. The
  158. // value Render passes is the command's own, see Result, so all that is left is to name
  159. // its type once here instead of asserting it at the top of every renderer. A renderer
  160. // paired with a command that returns something else fails here, saying what arrived.
  161. func Typed[T any](render func(T, io.Writer) error) TextFunc {
  162. return func(v any, w io.Writer) error {
  163. t, ok := v.(T)
  164. if !ok {
  165. return fmt.Errorf("expected %T, got %T", t, v)
  166. }
  167. return render(t, w)
  168. }
  169. }