| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- package commands
- import (
- "flag"
- "maps"
- "slices"
- )
- // Request is one command to run, as every surface hands it to Invoke: the command, its
- // options by name in the form their flags parse, its operands, and the rendering the
- // line asked for, if any.
- //
- // It is where the wire meets the daemon. nebula ctl parses its line against the schema
- // and ParseLine turns what was set into one of these, which travels as a JSON object
- // and becomes a Request again on the control socket. An ssh line never becomes one: it
- // is parsed straight into the command's own FlagSet, which Run then reads, so there is
- // nothing to carry. Both surfaces drive Run through the FlagSet commandFlags builds,
- // ssh by Parse and the socket by Set, and the flag package keeps those two in step.
- type Request struct {
- // Command is the command to run, resolved by the surface at its edge. See
- // Registry.Resolve for the one that tells an unknown name from a withheld one.
- Command *Command
- // Params carries the options the line set, by flag name, each in the form its flag
- // parses from. An option left alone is absent, so the command's default applies.
- Params map[string]string
- // Args carries the operands: whatever the line did not consume as a flag.
- Args []string
- // Format is the rendering the line asked for with -json or -pretty, or FormatUnset
- // when it expressed no preference. It is ParseLine's reading of the line for the
- // client that sends the Request; Invoke does not read it, since the surface that
- // runs a Request renders with the format it agreed with its caller.
- Format Format
- }
- // ParseLine reads a FlagSet that ParseArgs has filled, and the operands it returned,
- // into a Request to send over the wire. Command is left for the caller to set.
- //
- // Every flag the line set other than the presentation flags travels by name, as its
- // flag's String, which is what the daemon's Set parses. That round trip is exact for
- // the bool and string flags a client builds from the schema; it is the client's to make,
- // and the ssh path does not make it, since it runs the FlagSet it parsed.
- func ParseLine(fl *flag.FlagSet, operands []string) Request {
- req := Request{Args: operands, Format: presentationFormat(fl)}
- fl.Visit(func(f *flag.Flag) {
- if isPresentationFlag(f.Name) {
- return
- }
- if req.Params == nil {
- req.Params = map[string]string{}
- }
- req.Params[f.Name] = f.Value.String()
- })
- return req
- }
- // presentationFormat is the one reading of -json and -pretty from a parsed line, so a
- // line means the same on every surface: only a flag set to true asks for JSON, since one
- // set to false expresses no preference, and -pretty implies -json. FormatUnset means the
- // line expressed no preference and the surface's own default applies.
- func presentationFormat(fl *flag.FlagSet) Format {
- var asJSON, pretty bool
- fl.Visit(func(f *flag.Flag) {
- switch f.Name {
- case FlagJSON:
- asJSON = flagIsTrue(f)
- case FlagPretty:
- pretty = flagIsTrue(f)
- }
- })
- switch {
- case pretty:
- return FormatPrettyJSON
- case asJSON:
- return FormatJSON
- default:
- return FormatUnset
- }
- }
- // PresentationFormat is presentationFormat for a surface that takes a command's options
- // by name rather than from a line: the info API's query string. Only the two presentation
- // flags are read from params; the command's own parameters are the caller's to pass to
- // Invoke, which refuses these two among them. A flag given with no value counts as set,
- // as -pretty does on a line, and one that is not a boolean is a ParamError.
- func PresentationFormat(params map[string]string) (Format, error) {
- fl := flag.NewFlagSet("", flag.ContinueOnError)
- addPresentationFlags(fl)
- for _, k := range []string{FlagJSON, FlagPretty} {
- v, ok := params[k]
- if !ok {
- continue
- }
- if v == "" {
- v = "true"
- }
- if err := setFlag(fl, fl.Lookup(k), v); err != nil {
- return FormatUnset, err
- }
- }
- return presentationFormat(fl), nil
- }
- // flagIsTrue reports whether a bool flag was set to true. The flag package's bool value
- // is a flag.Getter whose Get returns the bool, so this never falls back to parsing text.
- func flagIsTrue(f *flag.Flag) bool {
- g, ok := f.Value.(flag.Getter)
- if !ok {
- return false
- }
- b, _ := g.Get().(bool)
- return b
- }
- // Invoke runs one Request and returns the command's Result, unrendered.
- //
- // The parameters are applied by flag name, in name order: a request from the wire is a
- // JSON object with no order of its own, and which of two bad parameters is reported must
- // not change from one request to the next. An unknown parameter is an error rather than
- // something quietly ignored, because a caller that misspells one deserves to hear about
- // it instead of getting a default. A bad parameter means Run is never called, and the
- // FlagSet is fresh for this call, so nothing set before it is seen by anyone.
- //
- // Nothing here writes anywhere and nothing here renders. What a surface does with the
- // Result, marshal its Payload or run the command's own text rendering through Render,
- // is the surface's decision, and keeping that out of here is what lets a caller tell a
- // refusal from a success after the text has been produced. The presentation flags are
- // not parameters: a Request says how it wants rendering in Format, and sending them as
- // parameters is refused rather than read as a preference.
- func Invoke(req Request) (Result, error) {
- c := req.Command
- fl, fs, own := commandFlags(c)
- if err := setParams(fl, string(c.Name), slices.Sorted(maps.Keys(req.Params)), req.Params, own); err != nil {
- return Result{}, err
- }
- return c.Run(fs, req.Args)
- }
|