request.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. package commands
  2. import (
  3. "flag"
  4. "maps"
  5. "slices"
  6. )
  7. // Request is one command to run, as every surface hands it to Invoke: the command, its
  8. // options by name in the form their flags parse, its operands, and the rendering the
  9. // line asked for, if any.
  10. //
  11. // It is where the wire meets the daemon. nebula ctl parses its line against the schema
  12. // and ParseLine turns what was set into one of these, which travels as a JSON object
  13. // and becomes a Request again on the control socket. An ssh line never becomes one: it
  14. // is parsed straight into the command's own FlagSet, which Run then reads, so there is
  15. // nothing to carry. Both surfaces drive Run through the FlagSet commandFlags builds,
  16. // ssh by Parse and the socket by Set, and the flag package keeps those two in step.
  17. type Request struct {
  18. // Command is the command to run, resolved by the surface at its edge. See
  19. // Registry.Resolve for the one that tells an unknown name from a withheld one.
  20. Command *Command
  21. // Params carries the options the line set, by flag name, each in the form its flag
  22. // parses from. An option left alone is absent, so the command's default applies.
  23. Params map[string]string
  24. // Args carries the operands: whatever the line did not consume as a flag.
  25. Args []string
  26. // Format is the rendering the line asked for with -json or -pretty, or FormatUnset
  27. // when it expressed no preference. It is ParseLine's reading of the line for the
  28. // client that sends the Request; Invoke does not read it, since the surface that
  29. // runs a Request renders with the format it agreed with its caller.
  30. Format Format
  31. }
  32. // ParseLine reads a FlagSet that ParseArgs has filled, and the operands it returned,
  33. // into a Request to send over the wire. Command is left for the caller to set.
  34. //
  35. // Every flag the line set other than the presentation flags travels by name, as its
  36. // flag's String, which is what the daemon's Set parses. That round trip is exact for
  37. // the bool and string flags a client builds from the schema; it is the client's to make,
  38. // and the ssh path does not make it, since it runs the FlagSet it parsed.
  39. func ParseLine(fl *flag.FlagSet, operands []string) Request {
  40. req := Request{Args: operands, Format: presentationFormat(fl)}
  41. fl.Visit(func(f *flag.Flag) {
  42. if isPresentationFlag(f.Name) {
  43. return
  44. }
  45. if req.Params == nil {
  46. req.Params = map[string]string{}
  47. }
  48. req.Params[f.Name] = f.Value.String()
  49. })
  50. return req
  51. }
  52. // presentationFormat is the one reading of -json and -pretty from a parsed line, so a
  53. // line means the same on every surface: only a flag set to true asks for JSON, since one
  54. // set to false expresses no preference, and -pretty implies -json. FormatUnset means the
  55. // line expressed no preference and the surface's own default applies.
  56. func presentationFormat(fl *flag.FlagSet) Format {
  57. var asJSON, pretty bool
  58. fl.Visit(func(f *flag.Flag) {
  59. switch f.Name {
  60. case FlagJSON:
  61. asJSON = flagIsTrue(f)
  62. case FlagPretty:
  63. pretty = flagIsTrue(f)
  64. }
  65. })
  66. switch {
  67. case pretty:
  68. return FormatPrettyJSON
  69. case asJSON:
  70. return FormatJSON
  71. default:
  72. return FormatUnset
  73. }
  74. }
  75. // PresentationFormat is presentationFormat for a surface that takes a command's options
  76. // by name rather than from a line: the info API's query string. Only the two presentation
  77. // flags are read from params; the command's own parameters are the caller's to pass to
  78. // Invoke, which refuses these two among them. A flag given with no value counts as set,
  79. // as -pretty does on a line, and one that is not a boolean is a ParamError.
  80. func PresentationFormat(params map[string]string) (Format, error) {
  81. fl := flag.NewFlagSet("", flag.ContinueOnError)
  82. addPresentationFlags(fl)
  83. for _, k := range []string{FlagJSON, FlagPretty} {
  84. v, ok := params[k]
  85. if !ok {
  86. continue
  87. }
  88. if v == "" {
  89. v = "true"
  90. }
  91. if err := setFlag(fl, fl.Lookup(k), v); err != nil {
  92. return FormatUnset, err
  93. }
  94. }
  95. return presentationFormat(fl), nil
  96. }
  97. // flagIsTrue reports whether a bool flag was set to true. The flag package's bool value
  98. // is a flag.Getter whose Get returns the bool, so this never falls back to parsing text.
  99. func flagIsTrue(f *flag.Flag) bool {
  100. g, ok := f.Value.(flag.Getter)
  101. if !ok {
  102. return false
  103. }
  104. b, _ := g.Get().(bool)
  105. return b
  106. }
  107. // Invoke runs one Request and returns the command's Result, unrendered.
  108. //
  109. // The parameters are applied by flag name, in name order: a request from the wire is a
  110. // JSON object with no order of its own, and which of two bad parameters is reported must
  111. // not change from one request to the next. An unknown parameter is an error rather than
  112. // something quietly ignored, because a caller that misspells one deserves to hear about
  113. // it instead of getting a default. A bad parameter means Run is never called, and the
  114. // FlagSet is fresh for this call, so nothing set before it is seen by anyone.
  115. //
  116. // Nothing here writes anywhere and nothing here renders. What a surface does with the
  117. // Result, marshal its Payload or run the command's own text rendering through Render,
  118. // is the surface's decision, and keeping that out of here is what lets a caller tell a
  119. // refusal from a success after the text has been produced. The presentation flags are
  120. // not parameters: a Request says how it wants rendering in Format, and sending them as
  121. // parameters is refused rather than read as a preference.
  122. func Invoke(req Request) (Result, error) {
  123. c := req.Command
  124. fl, fs, own := commandFlags(c)
  125. if err := setParams(fl, string(c.Name), slices.Sorted(maps.Keys(req.Params)), req.Params, own); err != nil {
  126. return Result{}, err
  127. }
  128. return c.Run(fs, req.Args)
  129. }