render.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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, to say which daemon to
  36. // talk to; see reservedFlags.
  37. const (
  38. FlagConfig = "config"
  39. FlagSocket = "socket"
  40. )
  41. // reservedFlags are handled before a command's Run is called, so a command may not
  42. // declare them itself. -h and -help are intercepted as a request for usage; -json and
  43. // -pretty are added to every command's FlagSet by commandFlags, on every surface, so a
  44. // command that defined either would make that definition panic with "flag redefined"
  45. // on whichever goroutine ran the command; and -config and -socket are nebula ctl's own,
  46. // taken off the line before the client knows which command it names, so a command
  47. // that declared either would never receive it there.
  48. var reservedFlags = []string{"h", "help", FlagJSON, FlagPretty, FlagConfig, FlagSocket}
  49. // Render writes r in the requested format. Every surface calls it, so a hostmap looks
  50. // the same over ssh as it does through the control socket, and a change to either
  51. // rendering happens once.
  52. func Render(c *Command, r Result, f Format, w io.Writer) error {
  53. switch f {
  54. case FormatJSON:
  55. return renderJSON(r.Payload(), w, false)
  56. case FormatPrettyJSON:
  57. return renderJSON(r.Payload(), w, true)
  58. default:
  59. // An outcome reads the same for every command: its reason, as a sentence. Only a
  60. // Value needs the command's own rendering, so Text never sees anything else.
  61. if r.Value == nil {
  62. _, err := io.WriteString(w, r.Reason+"\n")
  63. return err
  64. }
  65. if c != nil && c.Text != nil {
  66. return c.Text(r.Value, w)
  67. }
  68. name := "this command"
  69. if c != nil {
  70. name = string(c.Name)
  71. }
  72. return fmt.Errorf("%s has %w", name, ErrNoTextRendering)
  73. }
  74. }
  75. func renderJSON(v any, w io.Writer, indent bool) error {
  76. enc := json.NewEncoder(w)
  77. if indent {
  78. enc.SetIndent("", " ")
  79. }
  80. return enc.Encode(v)
  81. }
  82. // Result is what every Run returns: whether the command worked, a sentence saying so,
  83. // and, for a command that reports rather than acts, the value it produced.
  84. //
  85. // Value and Reason are alternatives. A command that reports returns Value(v), and the
  86. // surfaces render v, as JSON or through the command's Text. A command that acts, or any
  87. // command refusing its arguments, returns Did or Failed, and the surfaces render the
  88. // Outcome: its Reason is the sentence these commands have always printed. Nothing is for
  89. // a command with nothing to say.
  90. //
  91. // The envelope itself never reaches a caller. Payload is what JSON carries, and Render
  92. // hands Text only the Value, so a command's renderer sees the one type it returns on
  93. // success and is never asked to recognise an Outcome.
  94. type Result struct {
  95. Outcome
  96. Value any
  97. // NotFound marks a refusal that is about the thing asked for rather than the asking:
  98. // a vpn address with no tunnel behind it, as opposed to one that would not parse. It
  99. // renders exactly as any other refusal, and never reaches the wire; the distinction
  100. // is for a surface whose protocol has a word for it, so an HTTP client is told 404
  101. // rather than 400 and never has to parse the sentence.
  102. NotFound bool
  103. }
  104. // Outcome is the part of a Result that says whether the command worked and, in words a
  105. // caller can show someone, why. It is what a program receives from a command that acts
  106. // rather than reports: closing a tunnel, changing a remote, reloading config.
  107. //
  108. // One shape serves all of them. Designing a payload per outcome would be work without a
  109. // reader. Reason is set either way, so a success says what it did rather than leaving
  110. // the caller to infer it from a bare true.
  111. type Outcome struct {
  112. OK bool `json:"ok"`
  113. Reason string `json:"reason"`
  114. }
  115. // Value is the Result of a command that reports: v is what it has to show, and it must
  116. // marshal to JSON.
  117. func Value(v any) Result {
  118. return Result{Outcome: Outcome{OK: true}, Value: v}
  119. }
  120. // Failed is a convenience for the common "it did not work, here is why" case.
  121. func Failed(format string, args ...any) Result {
  122. return Result{Outcome: Outcome{Reason: fmt.Sprintf(format, args...)}}
  123. }
  124. // NotFound is Failed for a command asked about something that does not exist. See
  125. // Result.NotFound.
  126. func NotFound(format string, args ...any) Result {
  127. r := Failed(format, args...)
  128. r.NotFound = true
  129. return r
  130. }
  131. // Did is a convenience for the common "it worked, here is what happened" case.
  132. func Did(format string, args ...any) Result {
  133. return Result{Outcome: Outcome{OK: true, Reason: fmt.Sprintf(format, args...)}}
  134. }
  135. // Nothing is the Result of a command with nothing to say: it did its work, and the
  136. // surface sends an empty, successful reply without rendering anything.
  137. func Nothing() Result {
  138. return Result{Outcome: Outcome{OK: true}}
  139. }
  140. // Empty reports a Result with nothing to render: no Value and no Reason. The zero Result
  141. // counts, so a Run that returns Result{} says nothing rather than failing silently.
  142. func (r Result) Empty() bool {
  143. return r.Value == nil && r.Reason == ""
  144. }
  145. // Payload is what a program receives: the Value when there is one, otherwise the
  146. // Outcome. No surface sends the envelope itself, so print-tunnel -json emits the tunnel
  147. // and close-tunnel -json emits {ok, reason}.
  148. func (r Result) Payload() any {
  149. if r.Value != nil {
  150. return r.Value
  151. }
  152. return r.Outcome
  153. }
  154. // Typed adapts a renderer for one result type to the TextFunc a Command declares. The
  155. // value Render passes is the command's own, see Result, so all that is left is to name
  156. // its type once here instead of asserting it at the top of every renderer. A renderer
  157. // paired with a command that returns something else fails here, saying what arrived.
  158. func Typed[T any](render func(T, io.Writer) error) TextFunc {
  159. return func(v any, w io.Writer) error {
  160. t, ok := v.(T)
  161. if !ok {
  162. return fmt.Errorf("expected %T, got %T", t, v)
  163. }
  164. return render(t, w)
  165. }
  166. }