1
0

request.go 5.2 KB

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