From a63b646ea1ba6a6d65f9c7c5c52be56c13562903 Mon Sep 17 00:00:00 2001 From: Kevin Hoffman Date: Fri, 3 Nov 2023 14:09:58 -0400 Subject: [PATCH] nex info CLI command --- go.mod | 9 ++++-- go.sum | 13 ++++++++ nex-cli/cli.go | 4 +++ nex-cli/cmd/nex/main.go | 27 ++++++++-------- nex-cli/columns.go | 17 ++++++++++ nex-cli/conn.go | 28 ++++++++--------- nex-cli/nodes.go | 70 ++++++++++++++++++++++++++--------------- nex-cli/runner.go | 10 +++--- nex-node/controlapi.go | 10 +++--- 9 files changed, 122 insertions(+), 66 deletions(-) create mode 100644 nex-cli/columns.go diff --git a/go.mod b/go.mod index 74cb0d91..60e10123 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/containerd/fifo v1.0.0 // indirect github.com/containernetworking/cni v1.0.1 // indirect github.com/containernetworking/plugins v1.0.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/color v1.15.0 // indirect github.com/go-openapi/analysis v0.21.2 // indirect github.com/go-openapi/errors v0.20.2 // indirect @@ -46,15 +47,16 @@ require ( github.com/lightstep/tracecontext.go v0.0.0-20181129014701-1757c391b1ac // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/nats-io/jwt/v2 v2.5.3 // indirect + github.com/nats-io/natscli v0.1.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5 // indirect github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect go.mongodb.org/mongo-driver v1.8.3 // indirect @@ -65,6 +67,7 @@ require ( golang.org/x/crypto v0.14.0 // indirect golang.org/x/net v0.15.0 // indirect golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 95b25615..dcecba0c 100644 --- a/go.sum +++ b/go.sum @@ -254,6 +254,8 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= @@ -529,9 +531,13 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -574,6 +580,8 @@ github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E= github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8= +github.com/nats-io/natscli v0.1.1 h1:dZCXm710pax0HqSO+yV33DydCelmyidc+rqAIPtn+fE= +github.com/nats-io/natscli v0.1.1/go.mod h1:EOfShLSPML5BT1msb8UQZ67m/pEMPzpFJ2dg+q/s36Q= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY= @@ -685,6 +693,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -995,11 +1005,14 @@ golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/nex-cli/cli.go b/nex-cli/cli.go index b39b3641..036fa444 100644 --- a/nex-cli/cli.go +++ b/nex-cli/cli.go @@ -6,6 +6,10 @@ import ( "github.com/choria-io/fisk" ) +var ( + Opts = &Options{} +) + // Options configure the CLI type Options struct { Servers string diff --git a/nex-cli/cmd/nex/main.go b/nex-cli/cmd/nex/main.go index 5a1bcad6..cfe77ee8 100644 --- a/nex-cli/cmd/nex/main.go +++ b/nex-cli/cmd/nex/main.go @@ -10,7 +10,6 @@ import ( ) func main() { - opts := &cli.Options{} blue := color.New(color.FgBlue).SprintFunc() help := fmt.Sprintf("%s\nNATS Execution Engine CLI Version %s\n", blue(cli.Banner), cli.VERSION) @@ -21,30 +20,30 @@ func main() { ncli.HelpFlag.Short('h') ncli.WithCheats().CheatCommand.Hidden() - ncli.Flag("server", "NATS server urls").Short('s').Envar("NATS_URL").PlaceHolder("URL").StringVar(&opts.Servers) - ncli.Flag("user", "Username or Token").Envar("NATS_USER").PlaceHolder("USER").StringVar(&opts.Username) - ncli.Flag("password", "Password").Envar("NATS_PASSWORD").PlaceHolder("PASSWORD").StringVar(&opts.Password) - ncli.Flag("creds", "User credentials file (JWT authentication)").Envar("NATS_CREDS").PlaceHolder("FILE").StringVar(&opts.Creds) - ncli.Flag("nkey", "User NKEY file for single-key auth").Envar("NATS_NKEY").PlaceHolder("FILE").StringVar(&opts.Nkey) - ncli.Flag("tlscert", "TLS public certificate file").Envar("NATS_CERT").PlaceHolder("FILE").ExistingFileVar(&opts.TlsCert) - ncli.Flag("tlskey", "TLS private key file").Envar("NATS_KEY").PlaceHolder("FILE").ExistingFileVar(&opts.TlsKey) - ncli.Flag("tlsca", "TLS certificate authority chain file").Envar("NATS_CA").PlaceHolder("FILE").ExistingFileVar(&opts.TlsCA) - ncli.Flag("tlsfirst", "Perform TLS handshake before expecting the server greeting").BoolVar(&opts.TlsFirst) - ncli.Flag("timeout", "Time to wait on responses from NATS").Default("1s").Envar("NATS_TIMEOUT").PlaceHolder("DURATION").DurationVar(&opts.Timeout) + ncli.Flag("server", "NATS server urls").Short('s').Envar("NATS_URL").PlaceHolder("URL").StringVar(&cli.Opts.Servers) + ncli.Flag("user", "Username or Token").Envar("NATS_USER").PlaceHolder("USER").StringVar(&cli.Opts.Username) + ncli.Flag("password", "Password").Envar("NATS_PASSWORD").PlaceHolder("PASSWORD").StringVar(&cli.Opts.Password) + ncli.Flag("creds", "User credentials file (JWT authentication)").Envar("NATS_CREDS").PlaceHolder("FILE").StringVar(&cli.Opts.Creds) + ncli.Flag("nkey", "User NKEY file for single-key auth").Envar("NATS_NKEY").PlaceHolder("FILE").StringVar(&cli.Opts.Nkey) + ncli.Flag("tlscert", "TLS public certificate file").Envar("NATS_CERT").PlaceHolder("FILE").ExistingFileVar(&cli.Opts.TlsCert) + ncli.Flag("tlskey", "TLS private key file").Envar("NATS_KEY").PlaceHolder("FILE").ExistingFileVar(&cli.Opts.TlsKey) + ncli.Flag("tlsca", "TLS certificate authority chain file").Envar("NATS_CA").PlaceHolder("FILE").ExistingFileVar(&cli.Opts.TlsCA) + ncli.Flag("tlsfirst", "Perform TLS handshake before expecting the server greeting").BoolVar(&cli.Opts.TlsFirst) + ncli.Flag("timeout", "Time to wait on responses from NATS").Default("2s").Envar("NATS_TIMEOUT").PlaceHolder("DURATION").DurationVar(&cli.Opts.Timeout) nodes := ncli.Command("node", "Interact with execution engine nodes") nodes_ls := nodes.Command("ls", "List nodes") - nodes_ls.Action(cli.ListNodes(opts)) + nodes_ls.Action(cli.ListNodes) nodes_info := nodes.Command("info", "Get information for an engine node") nodes_info.Arg("id", "Public key of the node you're interested in").Required().String() - nodes_info.Action(cli.NodeInfo(opts)) + nodes_info.Action(cli.NodeInfo) run := ncli.Command("run", "Run a workload on a target node") run.Arg("id", "Public key of the node to run the workload").Required().String() run.Arg("file", "Path to local file to upload and run").File() run.Arg("url", "URL pointing to the file to run").URL() - run.Action(cli.RunWorkload(opts)) + run.Action(cli.RunWorkload) ncli.MustParseWithUsage(os.Args[1:]) } diff --git a/nex-cli/columns.go b/nex-cli/columns.go new file mode 100644 index 00000000..cb361a0d --- /dev/null +++ b/nex-cli/columns.go @@ -0,0 +1,17 @@ +package nexcli + +import ( + "github.com/nats-io/natscli/columns" +) + +func newColumns(heading string, a ...any) *columns.Writer { + w := columns.New(heading, a...) + w.SetColorScheme("cyan") + w.SetHeading(heading, a...) + + return w +} + +func f(v any) string { + return columns.F(v) +} diff --git a/nex-cli/conn.go b/nex-cli/conn.go index f2e4cbd6..cb88e552 100644 --- a/nex-cli/conn.go +++ b/nex-cli/conn.go @@ -7,27 +7,27 @@ import ( "github.com/nats-io/nats.go" ) -func generateConnectionFromOpts(opts *Options) (*nats.Conn, error) { - if len(strings.TrimSpace(opts.Servers)) == 0 { - opts.Servers = nats.DefaultURL +func generateConnectionFromOpts() (*nats.Conn, error) { + if len(strings.TrimSpace(Opts.Servers)) == 0 { + Opts.Servers = nats.DefaultURL } ctxOpts := []natscontext.Option{ - natscontext.WithServerURL(opts.Servers), - natscontext.WithCreds(opts.Creds), - natscontext.WithNKey(opts.Nkey), - natscontext.WithCertificate(opts.TlsCert), - natscontext.WithKey(opts.TlsKey), - natscontext.WithCA(opts.TlsCA), + natscontext.WithServerURL(Opts.Servers), + natscontext.WithCreds(Opts.Creds), + natscontext.WithNKey(Opts.Nkey), + natscontext.WithCertificate(Opts.TlsCert), + natscontext.WithKey(Opts.TlsKey), + natscontext.WithCA(Opts.TlsCA), } - if opts.TlsFirst { + if Opts.TlsFirst { ctxOpts = append(ctxOpts, natscontext.WithTLSHandshakeFirst()) } - if opts.Username != "" && opts.Password == "" { - ctxOpts = append(ctxOpts, natscontext.WithToken(opts.Username)) + if Opts.Username != "" && Opts.Password == "" { + ctxOpts = append(ctxOpts, natscontext.WithToken(Opts.Username)) } else { - ctxOpts = append(ctxOpts, natscontext.WithUser(opts.Username), natscontext.WithPassword(opts.Password)) + ctxOpts = append(ctxOpts, natscontext.WithUser(Opts.Username), natscontext.WithPassword(Opts.Password)) } natsContext, err := natscontext.New("nexnode", false, ctxOpts...) @@ -41,7 +41,7 @@ func generateConnectionFromOpts(opts *Options) (*nats.Conn, error) { return nil, err } - conn, err := nats.Connect(opts.Servers, natsOpts...) + conn, err := nats.Connect(Opts.Servers, natsOpts...) if err != nil { return nil, err } diff --git a/nex-cli/nodes.go b/nex-cli/nodes.go index 568bad4d..1e5a6906 100644 --- a/nex-cli/nodes.go +++ b/nex-cli/nodes.go @@ -2,46 +2,66 @@ package nexcli import ( "fmt" + "os" controlapi "github.com/ConnectEverything/nex/control-api" "github.com/choria-io/fisk" ) -func ListNodes(opts *Options) func(*fisk.ParseContext) error { - nc, err := generateConnectionFromOpts(opts) +func ListNodes(ctx *fisk.ParseContext) error { + + nc, err := generateConnectionFromOpts() if err != nil { - return errorClosure(err) + return err } - nodeClient := controlapi.NewApiClient(nc, opts.Timeout) - - return func(ctx *fisk.ParseContext) error { - nodes, err := nodeClient.ListNodes() - if err != nil { - return err - } - // TODO - renderNodeList(nodes) - return nil + nodeClient := controlapi.NewApiClient(nc, Opts.Timeout) + + nodes, err := nodeClient.ListNodes() + if err != nil { + return err } + // TODO + renderNodeList(nodes) + return nil + } -func NodeInfo(opts *Options) func(*fisk.ParseContext) error { - nc, err := generateConnectionFromOpts(opts) +func NodeInfo(ctx *fisk.ParseContext) error { + + nc, err := generateConnectionFromOpts() if err != nil { - return errorClosure(err) + return err } - nodeClient := controlapi.NewApiClient(nc, opts.Timeout) + nodeClient := controlapi.NewApiClient(nc, Opts.Timeout) + id := ctx.SelectedCommand.Model().Args[0].Value.String() + nodeInfo, err := nodeClient.NodeInfo(id) + if err != nil { + return err + } + renderNodeInfo(nodeInfo, id) + + return nil +} - return func(ctx *fisk.ParseContext) error { - id := ctx.SelectedCommand.Model().Args[0].Value.String() - nodeInfo, err := nodeClient.NodeInfo(id) - if err != nil { - return err - } - fmt.Printf("%v", nodeInfo) +func renderNodeInfo(info *controlapi.InfoResponse, id string) { + cols := newColumns("NEX Node Information") + defer cols.Frender(os.Stdout) + cols.AddRow("Node", id) + cols.AddRowf("Xkey", info.PublicXKey) + cols.AddRow("Version", info.Version) + cols.AddRow("Uptime", info.Uptime) - return nil + cols.AddSectionTitle("Workloads:") + cols.Indent(2) + for _, m := range info.Machines { + cols.Println() + cols.AddRow("Id", m.Id) + cols.AddRow("Healthy", m.Healthy) + cols.AddRow("Runtime", m.Uptime) + cols.AddRow("Name", m.Workload.Name) + cols.AddRow("Description", m.Workload.Description) } + cols.Indent(0) } func renderNodeList(nodes []controlapi.PingResponse) { diff --git a/nex-cli/runner.go b/nex-cli/runner.go index fecfec17..589c4c45 100644 --- a/nex-cli/runner.go +++ b/nex-cli/runner.go @@ -4,14 +4,12 @@ import ( "github.com/choria-io/fisk" ) -func RunWorkload(opts *Options) func(*fisk.ParseContext) error { - _, err := generateConnectionFromOpts(opts) +func RunWorkload(ctx *fisk.ParseContext) error { + _, err := generateConnectionFromOpts() if err != nil { - return errorClosure(err) + return err } // nodeClient := controlapi.NewApiClient(nc, opts.Timeout) + return nil - return func(ctx *fisk.ParseContext) error { - return nil - } } diff --git a/nex-node/controlapi.go b/nex-node/controlapi.go index b947e656..0070a8d7 100644 --- a/nex-node/controlapi.go +++ b/nex-node/controlapi.go @@ -140,12 +140,14 @@ func handlePing(api *ApiListener) func(m *nats.Msg) { func handleInfo(api *ApiListener) func(m *nats.Msg) { return func(m *nats.Msg) { + pubX, _ := api.xk.PublicKey() now := time.Now().UTC() res := controlapi.NewEnvelope(controlapi.InfoResponseType, controlapi.InfoResponse{ - Version: VERSION, - Uptime: myUptime(now.Sub(api.start)), - Tags: api.tags, - Machines: summarizeMachines(&api.mgr.allVms), + Version: VERSION, + PublicXKey: pubX, + Uptime: myUptime(now.Sub(api.start)), + Tags: api.tags, + Machines: summarizeMachines(&api.mgr.allVms), }, nil) raw, err := json.Marshal(res) if err != nil {