command.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. package commands
  2. import (
  3. "errors"
  4. "flag"
  5. "fmt"
  6. "io"
  7. "strings"
  8. )
  9. // CommandFlags is called before help or command execution to parse command line flags.
  10. // It should return a flag.FlagSet instance and a pointer to the struct that will contain parsed flags
  11. //
  12. // Every call must build a fresh FlagSet and a fresh struct. Nothing may be shared between
  13. // calls: a command runs on more than one surface and on more than one goroutine.
  14. type CommandFlags func() (*flag.FlagSet, any)
  15. // RunFunc computes a command's Result.
  16. //
  17. // fs is the pointer returned by Command.Flags, if there was one; -h, -help, -json and
  18. // -pretty are reserved and handled before Run is called. a is the operands: whatever
  19. // was not consumed as a flag. The Result is rendered by whichever surface asked: Value
  20. // for a command that reports, Did or Failed for one that acts, Nothing for one with
  21. // nothing to say. An error is for a failure of the command itself; a bad argument is
  22. // Failed, so the caller hears why in its own format.
  23. type RunFunc func(fs any, a []string) (Result, error)
  24. // TextFunc renders a Result's Value for a person. See Typed for declaring one against
  25. // the type the command actually returns.
  26. type TextFunc func(v any, w io.Writer) error
  27. type Command struct {
  28. Name Name
  29. ShortDescription string
  30. Help string
  31. ForProfiling bool
  32. // Identity declares that the command reports only what a certificate says about a
  33. // host: its name, addresses, groups and validity. Nothing operational: no underlay
  34. // addresses, no configuration, nothing a command could change. The info API, which
  35. // an operator may open to other users on the host, serves these and help alone; see
  36. // localapi.
  37. Identity bool
  38. Flags CommandFlags
  39. // Run produces the command's Result, leaving presentation to Render.
  40. Run RunFunc
  41. // Text renders a Result's Value for a person. Both surfaces call it, so a command
  42. // reads the same over ssh as it does through the control socket. It never sees an
  43. // outcome: Render prints those itself, the same way for every command. Leaving it
  44. // nil declares that no human rendering of the Value exists yet, which JSONOnly says
  45. // up front; Render then refuses text rather than inventing an interim form for
  46. // people to script against.
  47. Text TextFunc
  48. // JSONOnly declares that the command's Value has no human rendering yet, so a surface
  49. // whose default is text asks for JSON instead, and the schema says so to a client. It
  50. // is a statement that no such form exists, not a fallback. A refusal is still
  51. // rendered as a sentence, since that always has a human form.
  52. JSONOnly bool
  53. // Then, when set, is the second half of a command that acts: Run says what is about
  54. // to happen and Then does it and says how it went, with its Result rendered after
  55. // Run's and deciding the outcome. It exists for a command whose action can cut off
  56. // its own reply: reload restarts the ssh server when the sshd keys changed, which
  57. // ends the session that asked, so Run's line has to be on the wire before the reload
  58. // begins. A surface that answers in one piece, the control socket, runs Then before
  59. // it answers and carries both.
  60. Then func() Result
  61. }
  62. // Failure is the error a surface gets for a refusal the person has already been shown:
  63. // a Run that returned Failed, a command that is not there, a line that would not parse.
  64. // The message it rendered is the whole story, so the surface reports the failure, exit
  65. // status 1 over ssh, and does not log it.
  66. type Failure struct {
  67. Msg string
  68. }
  69. func (f *Failure) Error() string {
  70. return f.Msg
  71. }
  72. // commandFlags returns the FlagSet a command is driven through, on any surface: the
  73. // command's own flags, if it declares any, with the two presentation flags added. own
  74. // reports whether the command declared any of its own, which is what tells a caller
  75. // whether to say a parameter is unknown or that the command takes none at all.
  76. //
  77. // The presentation flags are defined here rather than by each command, because how a
  78. // result is displayed is not a property of the command that produced it. This is also
  79. // the one definition of them: the ssh line parses into it, nebula ctl parses its line
  80. // against the schema derived from it, and help renders from that schema. They are
  81. // command line flags only: a socket request chooses its rendering with the envelope's
  82. // format field, and Invoke refuses them as parameters.
  83. func commandFlags(c *Command) (fl *flag.FlagSet, fs any, own bool) {
  84. if c.Flags != nil {
  85. fl, fs = c.Flags()
  86. }
  87. own = fl != nil
  88. if fl == nil {
  89. fl = flag.NewFlagSet(string(c.Name), flag.ContinueOnError)
  90. }
  91. addPresentationFlags(fl)
  92. // What Parse prints after a bad flag, and for -h: the command's usage as help renders
  93. // it, so a line that went wrong is answered with the same text as `help <command>`.
  94. fl.Usage = func() { _ = WriteCommandHelp(schemaOf(c, fl), fl.Output()) }
  95. return fl, fs, own
  96. }
  97. // addPresentationFlags defines the two presentation flags on fl.
  98. func addPresentationFlags(fl *flag.FlagSet) {
  99. fl.Bool(FlagJSON, false, "outputs as json")
  100. fl.Bool(FlagPretty, false, "pretty prints json, assumes -json")
  101. }
  102. // Flagged fills in c's Flags and Run as a pair: define declares the flags on a fresh F,
  103. // and run is handed that F. The struct the flags fill is the struct Run reads by
  104. // construction, so no Run has to assert the type it was given or decide what to do
  105. // when the assertion fails.
  106. func Flagged[F any](c *Command, define func(*flag.FlagSet, *F), run func(*F, []string) (Result, error)) *Command {
  107. c.Flags = func() (*flag.FlagSet, any) {
  108. fl := flag.NewFlagSet("", flag.ContinueOnError)
  109. s := new(F)
  110. define(fl, s)
  111. return fl, s
  112. }
  113. c.Run = func(fs any, a []string) (Result, error) {
  114. s, ok := fs.(*F)
  115. if !ok {
  116. return Result{}, fmt.Errorf("%s was handed %T for its flags, not %T", c.Name, fs, s)
  117. }
  118. return run(s, a)
  119. }
  120. return c
  121. }
  122. // isPresentationFlag reports whether name is one of the two flags commandFlags adds.
  123. func isPresentationFlag(name string) bool {
  124. return name == FlagJSON || name == FlagPretty
  125. }
  126. // execLine runs a command line against c, reporting problems to w. It is the path from
  127. // an ssh line to output: the line is parsed against the command's own FlagSet and Run
  128. // reads that FlagSet's struct directly. Nothing is serialised and parsed again on the
  129. // way, so a flag.Value whose String does not re-parse, flag.Func above all, is applied
  130. // as the line said and not as its String says. The control socket fills the same
  131. // FlagSet through Set in Invoke; Parse and Set are the flag package's one parser.
  132. func (r *Registry) execLine(c *Command, args []string, w Output) error {
  133. fl, fs, _ := commandFlags(c)
  134. // Parse reports a bad flag to this writer, with the command's usage after it. -h,
  135. // -help and --help take the same path with nothing wrong: the usage is the whole
  136. // answer, and Parse says so with ErrHelp. A -h that is a flag's value or follows --
  137. // is read as the operand it is, since ParseArgs is the one reading of the line.
  138. fl.SetOutput(w.Err)
  139. operands, err := ParseArgs(fl, args)
  140. if err != nil {
  141. if errors.Is(err, flag.ErrHelp) {
  142. return nil
  143. }
  144. return err
  145. }
  146. format := presentationFormat(fl)
  147. res, err := c.Run(fs, operands)
  148. if err != nil {
  149. return err
  150. }
  151. // Run's reply goes out first, and then the action, if the command has one, whose
  152. // Result is the outcome. In JSON only the outcome travels: the promise is for a
  153. // person to read, and a program expects one document.
  154. if c.Then == nil || r.formatFor(c, res, format) == FormatText {
  155. if err := r.renderLine(c, res, format, w); err != nil {
  156. return err
  157. }
  158. }
  159. if c.Then != nil {
  160. res = c.Then()
  161. if err := r.renderLine(c, res, format, w); err != nil {
  162. return err
  163. }
  164. }
  165. if !res.OK {
  166. return &Failure{Msg: res.Reason}
  167. }
  168. return nil
  169. }
  170. // formatFor is the format a Result is rendered in. The line wins when it asked for one;
  171. // otherwise the surface that owns the registry decides, from the command and its
  172. // Result, which is how ssh keeps printing what it always has while another surface can
  173. // choose differently.
  174. func (r *Registry) formatFor(c *Command, res Result, format Format) Format {
  175. if format == FormatUnset {
  176. return r.defaultFormatFor(c, res)
  177. }
  178. return format
  179. }
  180. // renderLine writes one Result of a command line to w; see formatFor. An empty Result
  181. // writes nothing.
  182. func (r *Registry) renderLine(c *Command, res Result, format Format, w Output) error {
  183. if res.Empty() {
  184. return nil
  185. }
  186. if err := Render(c, res, r.formatFor(c, res, format), w.Out); err != nil {
  187. if errors.Is(err, ErrNoTextRendering) {
  188. _, _ = fmt.Fprintln(w.Err, err.Error())
  189. }
  190. return err
  191. }
  192. return nil
  193. }
  194. // ParseArgs parses args with fl and returns the operands.
  195. //
  196. // It differs from fl.Parse in two ways, and both surfaces parse through here so a line
  197. // means the same typed at an ssh prompt or to nebula ctl.
  198. //
  199. // Flags and operands may come in any order, which is the one way it differs from
  200. // fl.Parse. Parse stops at the first operand, so `print-tunnel 10.0.0.1 -json` would
  201. // reach the command with "-json" as a second operand and the flag silently dropped. Here
  202. // an operand ends nothing: parsing resumes after it, and only "--" ends the flags, with
  203. // everything after it an operand. What counts as a flag is Parse's rule: every argument
  204. // that starts with "-", a negative number included, so one that is meant as an operand
  205. // goes after "--".
  206. func ParseArgs(fl *flag.FlagSet, args []string) ([]string, error) {
  207. operands := make([]string, 0, len(args))
  208. for len(args) > 0 {
  209. // Walk the flags the way Parse will, to the first argument it would not read as a
  210. // flag: an operand or the terminator. Everything before it is Parse's.
  211. end := len(args)
  212. for i := 0; i < len(args); {
  213. a := args[i]
  214. if a == "--" {
  215. if err := fl.Parse(args[:i]); err != nil {
  216. return nil, err
  217. }
  218. return append(operands, args[i+1:]...), nil
  219. }
  220. if len(a) < 2 || a[0] != '-' {
  221. end = i
  222. break
  223. }
  224. f, hasValue := LookupArg(fl, a)
  225. if f == nil {
  226. // An undefined flag: leave it for Parse to report.
  227. i++
  228. continue
  229. }
  230. if hasValue || IsBoolFlag(f) {
  231. i++
  232. } else {
  233. // The flag's value is the next argument, whatever it looks like.
  234. i += 2
  235. }
  236. }
  237. if err := fl.Parse(args[:end]); err != nil {
  238. return nil, err
  239. }
  240. if end == len(args) {
  241. break
  242. }
  243. operands = append(operands, args[end])
  244. args = args[end+1:]
  245. }
  246. return operands, nil
  247. }
  248. // LookupArg reads arg the way Parse does: one or two leading dashes, the flag's name, and
  249. // an optional "=value". f is the flag of fl by that name, nil when fl has none, and
  250. // hasValue reports that the value came with the argument rather than in the one after it.
  251. // It is the one reading of an argument's shape: ParseArgs walks a line with it, and
  252. // nebula ctl lifts its own flags out of a line with it, so the two cannot disagree about
  253. // which word is a flag's value.
  254. func LookupArg(fl *flag.FlagSet, arg string) (f *flag.Flag, hasValue bool) {
  255. name := strings.TrimPrefix(strings.TrimPrefix(arg, "-"), "-")
  256. if eq := strings.IndexByte(name, '='); eq >= 0 {
  257. name, hasValue = name[:eq], true
  258. }
  259. return fl.Lookup(name), hasValue
  260. }
  261. // IsBoolFlag reports whether f is satisfied by its presence alone and never reads the
  262. // argument after it as its value. The flag package marks these with an unexported
  263. // interface, which is the only way to tell.
  264. func IsBoolFlag(f *flag.Flag) bool {
  265. bf, ok := f.Value.(interface{ IsBoolFlag() bool })
  266. return ok && bf.IsBoolFlag()
  267. }