mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 20:16:52 +01:00
Add a local control socket and nebula ctl (#1975)
Add support for accessing the previously ssh-only debug commands via `nebula ctl <command>.` - unix socket (named pipe on windows) for communication - sockets scoped to the user + an optional group - in cmd/nebula and cmd/nebula-service, but not nebula.Main Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
9a5bdb508e
commit
0f767ce529
54 files changed
+7581
-925
No files matched your search
@@ -0,0 +1,179 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// ErrNoTextRendering is returned by Render when a command's Value has no human rendering
|
||||
// and text was asked for anyway. A command declares this by leaving Text nil and saying
|
||||
// so with JSONOnly; it is not a fallback situation but a statement that no such form
|
||||
// exists yet, so nobody builds a script around an interim one. Each surface's default
|
||||
// policy avoids ever asking.
|
||||
var ErrNoTextRendering = errors.New("no human readable output yet; pass -json or -pretty")
|
||||
|
||||
// Format is how a result should be presented.
|
||||
type Format int
|
||||
|
||||
const (
|
||||
// FormatUnset is a request that expressed no preference. A surface resolves it with
|
||||
// its own default before rendering; Render itself treats it as FormatText.
|
||||
FormatUnset Format = iota
|
||||
|
||||
// FormatText renders for a person. It is what an operator gets by default on every
|
||||
// surface with a human at the other end.
|
||||
FormatText
|
||||
|
||||
// FormatJSON renders the result as compact JSON.
|
||||
FormatJSON
|
||||
|
||||
// FormatPrettyJSON renders the result as indented JSON.
|
||||
FormatPrettyJSON
|
||||
)
|
||||
|
||||
// Flag names for the two presentation flags. They are offered on every command rather
|
||||
// than declared by each one, because how a result is displayed is not a property of the
|
||||
// command that produced it.
|
||||
const (
|
||||
FlagJSON = "json"
|
||||
FlagPretty = "pretty"
|
||||
)
|
||||
|
||||
// Flag names nebula ctl takes for itself, anywhere on its line, to say which daemon to
|
||||
// talk to; see reservedFlags.
|
||||
const (
|
||||
FlagConfig = "config"
|
||||
FlagSocket = "socket"
|
||||
)
|
||||
|
||||
// reservedFlags are handled before a command's Run is called, so a command may not
|
||||
// declare them itself. -h and -help are intercepted as a request for usage; -json and
|
||||
// -pretty are added to every command's FlagSet by commandFlags, on every surface, so a
|
||||
// command that defined either would make that definition panic with "flag redefined"
|
||||
// on whichever goroutine ran the command; and -config and -socket are nebula ctl's own,
|
||||
// taken off the line before the client knows which command it names, so a command
|
||||
// that declared either would never receive it there.
|
||||
var reservedFlags = []string{"h", "help", FlagJSON, FlagPretty, FlagConfig, FlagSocket}
|
||||
|
||||
// Render writes r in the requested format. Every surface calls it, so a hostmap looks
|
||||
// the same over ssh as it does through the control socket, and a change to either
|
||||
// rendering happens once.
|
||||
func Render(c *Command, r Result, f Format, w io.Writer) error {
|
||||
switch f {
|
||||
case FormatJSON:
|
||||
return renderJSON(r.Payload(), w, false)
|
||||
|
||||
case FormatPrettyJSON:
|
||||
return renderJSON(r.Payload(), w, true)
|
||||
|
||||
default:
|
||||
// An outcome reads the same for every command: its reason, as a sentence. Only a
|
||||
// Value needs the command's own rendering, so Text never sees anything else.
|
||||
if r.Value == nil {
|
||||
_, err := io.WriteString(w, r.Reason+"\n")
|
||||
return err
|
||||
}
|
||||
|
||||
if c != nil && c.Text != nil {
|
||||
return c.Text(r.Value, w)
|
||||
}
|
||||
|
||||
name := "this command"
|
||||
if c != nil {
|
||||
name = string(c.Name)
|
||||
}
|
||||
return fmt.Errorf("%s has %w", name, ErrNoTextRendering)
|
||||
}
|
||||
}
|
||||
|
||||
func renderJSON(v any, w io.Writer, indent bool) error {
|
||||
enc := json.NewEncoder(w)
|
||||
if indent {
|
||||
enc.SetIndent("", " ")
|
||||
}
|
||||
|
||||
return enc.Encode(v)
|
||||
}
|
||||
|
||||
// Result is what every Run returns: whether the command worked, a sentence saying so,
|
||||
// and, for a command that reports rather than acts, the value it produced.
|
||||
//
|
||||
// Value and Reason are alternatives. A command that reports returns Value(v), and the
|
||||
// surfaces render v, as JSON or through the command's Text. A command that acts, or any
|
||||
// command refusing its arguments, returns Did or Failed, and the surfaces render the
|
||||
// Outcome: its Reason is the sentence these commands have always printed. Nothing is for
|
||||
// a command with nothing to say.
|
||||
//
|
||||
// The envelope itself never reaches a caller. Payload is what JSON carries, and Render
|
||||
// hands Text only the Value, so a command's renderer sees the one type it returns on
|
||||
// success and is never asked to recognise an Outcome.
|
||||
type Result struct {
|
||||
Outcome
|
||||
Value any
|
||||
}
|
||||
|
||||
// Outcome is the part of a Result that says whether the command worked and, in words a
|
||||
// caller can show someone, why. It is what a program receives from a command that acts
|
||||
// rather than reports: closing a tunnel, changing a remote, reloading config.
|
||||
//
|
||||
// One shape serves all of them. Designing a payload per outcome would be work without a
|
||||
// reader. Reason is set either way, so a success says what it did rather than leaving
|
||||
// the caller to infer it from a bare true.
|
||||
type Outcome struct {
|
||||
OK bool `json:"ok"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// Value is the Result of a command that reports: v is what it has to show, and it must
|
||||
// marshal to JSON.
|
||||
func Value(v any) Result {
|
||||
return Result{Outcome: Outcome{OK: true}, Value: v}
|
||||
}
|
||||
|
||||
// Failed is a convenience for the common "it did not work, here is why" case.
|
||||
func Failed(format string, args ...any) Result {
|
||||
return Result{Outcome: Outcome{Reason: fmt.Sprintf(format, args...)}}
|
||||
}
|
||||
|
||||
// Did is a convenience for the common "it worked, here is what happened" case.
|
||||
func Did(format string, args ...any) Result {
|
||||
return Result{Outcome: Outcome{OK: true, Reason: fmt.Sprintf(format, args...)}}
|
||||
}
|
||||
|
||||
// Nothing is the Result of a command with nothing to say: it did its work, and the
|
||||
// surface sends an empty, successful reply without rendering anything.
|
||||
func Nothing() Result {
|
||||
return Result{Outcome: Outcome{OK: true}}
|
||||
}
|
||||
|
||||
// Empty reports a Result with nothing to render: no Value and no Reason. The zero Result
|
||||
// counts, so a Run that returns Result{} says nothing rather than failing silently.
|
||||
func (r Result) Empty() bool {
|
||||
return r.Value == nil && r.Reason == ""
|
||||
}
|
||||
|
||||
// Payload is what a program receives: the Value when there is one, otherwise the
|
||||
// Outcome. No surface sends the envelope itself, so print-tunnel -json emits the tunnel
|
||||
// and close-tunnel -json emits {ok, reason}.
|
||||
func (r Result) Payload() any {
|
||||
if r.Value != nil {
|
||||
return r.Value
|
||||
}
|
||||
return r.Outcome
|
||||
}
|
||||
|
||||
// Typed adapts a renderer for one result type to the TextFunc a Command declares. The
|
||||
// value Render passes is the command's own, see Result, so all that is left is to name
|
||||
// its type once here instead of asserting it at the top of every renderer. A renderer
|
||||
// paired with a command that returns something else fails here, saying what arrived.
|
||||
func Typed[T any](render func(T, io.Writer) error) TextFunc {
|
||||
return func(v any, w io.Writer) error {
|
||||
t, ok := v.(T)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected %T, got %T", t, v)
|
||||
}
|
||||
return render(t, w)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user