From 43c189292427175aff335b463dfb8a76e6bc3f68 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Tue, 16 Mar 2021 11:00:47 +0100 Subject: [PATCH 1/3] Provide CLI commands for querying node status and diagnostics --- cmd/root.go | 4 +- core/status/cmd.go | 61 +++++++++++++++ core/status/cmd_test.go | 78 +++++++++++++++++++ core/{status.go => status/engine.go} | 31 ++++---- .../{status_test.go => status/engine_test.go} | 11 +-- 5 files changed, 165 insertions(+), 20 deletions(-) create mode 100644 core/status/cmd.go create mode 100644 core/status/cmd_test.go rename core/{status.go => status/engine.go} (67%) rename core/{status_test.go => status/engine_test.go} (86%) diff --git a/cmd/root.go b/cmd/root.go index 752ff3402b..d673b13269 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -25,6 +25,7 @@ import ( authIrmaAPI "github.com/nuts-foundation/nuts-node/auth/api/irma" authV1API "github.com/nuts-foundation/nuts-node/auth/api/v0" authCmd "github.com/nuts-foundation/nuts-node/auth/cmd" + "github.com/nuts-foundation/nuts-node/core/status" "io" "os" @@ -144,7 +145,7 @@ func CreateSystem() *core.System { networkInstance := network.NewNetworkInstance(network.DefaultConfig(), cryptoInstance) vdrInstance := vdr.NewVDR(vdr.DefaultConfig(), cryptoInstance, networkInstance) credentialInstance := vcr.NewVCRInstance(cryptoInstance, vdrInstance, networkInstance) - statusEngine := core.NewStatusEngine(system) + statusEngine := status.NewStatusEngine(system) metricsEngine := core.NewMetricsEngine() authInstance := auth.NewAuthInstance(auth.DefaultConfig(), vdrInstance, cryptoInstance) @@ -182,6 +183,7 @@ func Execute(system *core.System) { func addSubCommands(system *core.System, root *cobra.Command) { // Register client commands clientCommands := []*cobra.Command{ + status.Cmd(), cryptoCmd.Cmd(), networkCmd.Cmd(), vdrCmd.Cmd(), diff --git a/core/status/cmd.go b/core/status/cmd.go new file mode 100644 index 0000000000..0d0c7607f8 --- /dev/null +++ b/core/status/cmd.go @@ -0,0 +1,61 @@ +/* + * Nuts node + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package status + +import ( + "fmt" + "github.com/spf13/cobra" + "io/ioutil" + "net/http" + + "github.com/nuts-foundation/nuts-node/core" +) + +// Cmd contains sub-commands for the remote client +func Cmd() *cobra.Command { + var verbose bool + cmd := &cobra.Command{ + Use: "status", + Short: "Shows the status of the Nuts Node.", + RunE: func(cmd *cobra.Command, args []string) error { + config := core.NewClientConfig() + if err := config.Load(cmd.PersistentFlags()); err != nil { + return err + } + var targetURL string + if verbose { + targetURL = config.GetAddress() + diagnosticsEndpoint + } else { + targetURL = config.GetAddress() + statusEndpoint + } + response, err := http.Get(targetURL) + if err != nil { + return err + } + if response.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected HTTP response code (url=%s): %d", targetURL, response.StatusCode) + } + bytes, _ := ioutil.ReadAll(response.Body) + cmd.Println(string(bytes)) + return nil + }, + } + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Pass 'true' to show verbose diagnostics.") + return cmd +} diff --git a/core/status/cmd_test.go b/core/status/cmd_test.go new file mode 100644 index 0000000000..6a7f1e5220 --- /dev/null +++ b/core/status/cmd_test.go @@ -0,0 +1,78 @@ +/* + * Nuts node + * Copyright (C) 2021. Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package status + +import ( + "bytes" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/nuts-foundation/nuts-node/core" + http2 "github.com/nuts-foundation/nuts-node/test/http" + "github.com/stretchr/testify/assert" +) + +func TestEngine_Command(t *testing.T) { + t.Run("status", func(t *testing.T) { + cmd := Cmd() + s := httptest.NewServer(http2.Handler{StatusCode: http.StatusOK, ResponseData: "OK"}) + os.Setenv("NUTS_ADDRESS", s.URL) + defer os.Unsetenv("NUTS_ADDRESS") + core.NewServerConfig().Load(cmd) + defer s.Close() + + buf := new(bytes.Buffer) + cmd.SetArgs([]string{"status"}) + cmd.SetOut(buf) + err := cmd.Execute() + assert.NoError(t, err) + assert.Equal(t, "OK\n", buf.String()) + }) + t.Run("verbose", func(t *testing.T) { + cmd := Cmd() + s := httptest.NewServer(http2.Handler{StatusCode: http.StatusOK, ResponseData: "diagnostics"}) + os.Setenv("NUTS_ADDRESS", s.URL) + defer os.Unsetenv("NUTS_ADDRESS") + core.NewServerConfig().Load(cmd) + defer s.Close() + + buf := new(bytes.Buffer) + cmd.SetArgs([]string{"status", "-v"}) + cmd.SetOut(buf) + err := cmd.Execute() + assert.NoError(t, err) + assert.Equal(t, "diagnostics\n", buf.String()) + }) + t.Run("unexpected status code", func(t *testing.T) { + cmd := Cmd() + s := httptest.NewServer(http2.Handler{StatusCode: http.StatusInternalServerError, ResponseData: ""}) + os.Setenv("NUTS_ADDRESS", s.URL) + defer os.Unsetenv("NUTS_ADDRESS") + core.NewServerConfig().Load(cmd) + defer s.Close() + + buf := new(bytes.Buffer) + cmd.SetArgs([]string{"status"}) + cmd.SetOut(buf) + err := cmd.Execute() + assert.Error(t, err) + }) +} diff --git a/core/status.go b/core/status/engine.go similarity index 67% rename from core/status.go rename to core/status/engine.go index 592e4340e3..b84391795f 100644 --- a/core/status.go +++ b/core/status/engine.go @@ -17,10 +17,11 @@ * */ -package core +package status import ( "fmt" + "github.com/nuts-foundation/nuts-node/core" "net/http" "strings" "time" @@ -29,14 +30,16 @@ import ( ) const moduleName = "Status" +const diagnosticsEndpoint = "/status/diagnostics" +const statusEndpoint = "/status" type status struct { - system *System + system *core.System startTime time.Time } //NewStatusEngine creates a new Engine for viewing all engines -func NewStatusEngine(system *System) Engine { +func NewStatusEngine(system *core.System) core.Engine { return &status{ system: system, startTime: time.Now(), @@ -47,9 +50,9 @@ func (s *status) Name() string { return moduleName } -func (s *status) Routes(router EchoRouter) { - router.Add(http.MethodGet, "/status/diagnostics", s.diagnosticsOverview) - router.Add(http.MethodGet, "/status", statusOK) +func (s *status) Routes(router core.EchoRouter) { + router.Add(http.MethodGet, diagnosticsEndpoint, s.diagnosticsOverview) + router.Add(http.MethodGet, statusEndpoint, statusOK) } func (s *status) diagnosticsOverview(ctx echo.Context) error { @@ -58,8 +61,8 @@ func (s *status) diagnosticsOverview(ctx echo.Context) error { func (s *status) diagnosticsSummaryAsText() string { var lines []string - s.system.VisitEngines(func(engine Engine) { - if m, ok := engine.(ViewableDiagnostics); ok { + s.system.VisitEngines(func(engine core.Engine) { + if m, ok := engine.(core.ViewableDiagnostics); ok { lines = append(lines, m.Name()) diagnostics := m.Diagnostics() for _, d := range diagnostics { @@ -72,17 +75,17 @@ func (s *status) diagnosticsSummaryAsText() string { // Diagnostics returns list of DiagnosticResult for the StatusEngine. // The results are a list of all registered engines -func (s *status) Diagnostics() []DiagnosticResult { - return []DiagnosticResult{ - &GenericDiagnosticResult{Title: "Registered engines", Outcome: strings.Join(s.listAllEngines(), ",")}, - &GenericDiagnosticResult{Title: "Uptime", Outcome: time.Now().Sub(s.startTime).String()}, +func (s *status) Diagnostics() []core.DiagnosticResult { + return []core.DiagnosticResult{ + &core.GenericDiagnosticResult{Title: "Registered engines", Outcome: strings.Join(s.listAllEngines(), ",")}, + &core.GenericDiagnosticResult{Title: "Uptime", Outcome: time.Now().Sub(s.startTime).String()}, } } func (s *status) listAllEngines() []string { var names []string - s.system.VisitEngines(func(engine Engine) { - if m, ok := engine.(Named); ok { + s.system.VisitEngines(func(engine core.Engine) { + if m, ok := engine.(core.Named); ok { names = append(names, m.Name()) } }) diff --git a/core/status_test.go b/core/status/engine_test.go similarity index 86% rename from core/status_test.go rename to core/status/engine_test.go index fde858c00c..78b40d014e 100644 --- a/core/status_test.go +++ b/core/status/engine_test.go @@ -1,6 +1,7 @@ -package core +package status import ( + "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/test" "net/http" "testing" @@ -14,19 +15,19 @@ func TestNewStatusEngine_Routes(t *testing.T) { t.Run("Registers a single route for listing all engines", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - echo := NewMockEchoRouter(ctrl) + echo := core.NewMockEchoRouter(ctrl) echo.EXPECT().Add(http.MethodGet, "/status/diagnostics", gomock.Any()) echo.EXPECT().Add(http.MethodGet, "/status", gomock.Any()) - NewStatusEngine(NewSystem()).(*status).Routes(echo) + NewStatusEngine(core.NewSystem()).(*status).Routes(echo) }) } func TestNewStatusEngine_Diagnostics(t *testing.T) { - system := NewSystem() + system := core.NewSystem() system.RegisterEngine(NewStatusEngine(system)) - system.RegisterEngine(NewMetricsEngine()) + system.RegisterEngine(core.NewMetricsEngine()) t.Run("diagnostics() returns engine list", func(t *testing.T) { system := NewStatusEngine(system) From c93a3b2903c7c9a50ccbdb2e41afea61c9e03a40 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Tue, 16 Mar 2021 11:14:22 +0100 Subject: [PATCH 2/3] Added test --- core/metrics_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/metrics_test.go b/core/metrics_test.go index d329e5296f..c5335f4e8f 100644 --- a/core/metrics_test.go +++ b/core/metrics_test.go @@ -31,6 +31,11 @@ import ( "github.com/stretchr/testify/assert" ) +func TestMetricsEngine_Name(t *testing.T) { + named := NewMetricsEngine().(Named) + assert.Equal(t, "Metrics", named.Name()) +} + func TestNewMetricsEngine(t *testing.T) { mEngine := NewMetricsEngine().(*metrics) _ = mEngine.Configure(ServerConfig{}) From 3213c48b4409ef26a1fe0df9949a6c8313ccd4a8 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Tue, 16 Mar 2021 12:25:12 +0100 Subject: [PATCH 3/3] PR feedback --- core/status/cmd.go | 9 +-------- core/status/cmd_test.go | 19 ++----------------- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/core/status/cmd.go b/core/status/cmd.go index 0d0c7607f8..d981fccb2a 100644 --- a/core/status/cmd.go +++ b/core/status/cmd.go @@ -29,7 +29,6 @@ import ( // Cmd contains sub-commands for the remote client func Cmd() *cobra.Command { - var verbose bool cmd := &cobra.Command{ Use: "status", Short: "Shows the status of the Nuts Node.", @@ -38,12 +37,7 @@ func Cmd() *cobra.Command { if err := config.Load(cmd.PersistentFlags()); err != nil { return err } - var targetURL string - if verbose { - targetURL = config.GetAddress() + diagnosticsEndpoint - } else { - targetURL = config.GetAddress() + statusEndpoint - } + targetURL := config.GetAddress() + diagnosticsEndpoint response, err := http.Get(targetURL) if err != nil { return err @@ -56,6 +50,5 @@ func Cmd() *cobra.Command { return nil }, } - cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Pass 'true' to show verbose diagnostics.") return cmd } diff --git a/core/status/cmd_test.go b/core/status/cmd_test.go index 6a7f1e5220..2e20b876ad 100644 --- a/core/status/cmd_test.go +++ b/core/status/cmd_test.go @@ -31,22 +31,7 @@ import ( ) func TestEngine_Command(t *testing.T) { - t.Run("status", func(t *testing.T) { - cmd := Cmd() - s := httptest.NewServer(http2.Handler{StatusCode: http.StatusOK, ResponseData: "OK"}) - os.Setenv("NUTS_ADDRESS", s.URL) - defer os.Unsetenv("NUTS_ADDRESS") - core.NewServerConfig().Load(cmd) - defer s.Close() - - buf := new(bytes.Buffer) - cmd.SetArgs([]string{"status"}) - cmd.SetOut(buf) - err := cmd.Execute() - assert.NoError(t, err) - assert.Equal(t, "OK\n", buf.String()) - }) - t.Run("verbose", func(t *testing.T) { + t.Run("ok", func(t *testing.T) { cmd := Cmd() s := httptest.NewServer(http2.Handler{StatusCode: http.StatusOK, ResponseData: "diagnostics"}) os.Setenv("NUTS_ADDRESS", s.URL) @@ -55,7 +40,7 @@ func TestEngine_Command(t *testing.T) { defer s.Close() buf := new(bytes.Buffer) - cmd.SetArgs([]string{"status", "-v"}) + cmd.SetArgs([]string{"status"}) cmd.SetOut(buf) err := cmd.Execute() assert.NoError(t, err)