Repository: code-ready/crc
Pull request #2100 (2021-03-15 11:30:09)

Retain 100 log messages in the memory and serve them through the API
The goal with this API is to add a log text area in the tray. It will show more details to the user than the status.

openshift-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/code-ready/crc/pull/2100#" title="Author self-approved">guillaumerose</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=code-ready%2Fcrc).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/code-ready/crc/blob/master/OWNERS)~~ [guillaumerose]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
openshift-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/code-ready/crc/pull/2100#pullrequestreview-612866741" title="Approved">anjannath</a>*, *<a href="https://github.com/code-ready/crc/pull/2100#" title="Author self-approved">guillaumerose</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=code-ready%2Fcrc).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/code-ready/crc/blob/master/OWNERS)~~ [anjannath,guillaumerose]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/cmd/crc/cmd/daemon.go b/cmd/crc/cmd/daemon.go
index b7b15dbcb..a5e7bea32 100644
--- a/cmd/crc/cmd/daemon.go
+++ b/cmd/crc/cmd/daemon.go
@@ -14,6 +14,7 @@ import (
 	cmdConfig "github.com/code-ready/crc/cmd/crc/cmd/config"
 	"github.com/code-ready/crc/pkg/crc/api"
 	"github.com/code-ready/crc/pkg/crc/constants"
+	"github.com/code-ready/crc/pkg/crc/logging"
 	"github.com/code-ready/gvisor-tap-vsock/pkg/types"
 	"github.com/code-ready/gvisor-tap-vsock/pkg/virtualnetwork"
 	"github.com/docker/go-units"
@@ -162,7 +163,7 @@ func run(configuration *types.Configuration) error {
 func runDaemon() error {
 	// Remove if an old socket is present
 	os.Remove(constants.DaemonSocketPath)
-	apiServer, err := api.CreateServer(constants.DaemonSocketPath, config, newMachine())
+	apiServer, err := api.CreateServer(constants.DaemonSocketPath, config, newMachine(), logging.Memory)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/crc/api/adapter.go b/pkg/crc/api/adapter.go
index 00e50cd3c..1334d592c 100644
--- a/pkg/crc/api/adapter.go
+++ b/pkg/crc/api/adapter.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/code-ready/crc/pkg/crc/logging"
 	"github.com/code-ready/crc/pkg/crc/machine"
+	"github.com/code-ready/machine/libmachine/state"
 )
 
 type AdaptedClient interface {
@@ -126,13 +127,19 @@ func (a *Adapter) Status() ClusterStatusResult {
 }
 
 func (a *Adapter) Stop() Result {
-	_, err := a.Underlying.Stop()
+	vmState, err := a.Underlying.Stop()
 	if err != nil {
 		logging.Error(err)
-		return Result{
-			Name:    a.Underlying.GetName(),
-			Success: false,
-			Error:   err.Error(),
+		if vmState == state.Running {
+			err := a.Underlying.PowerOff()
+			if err != nil {
+				logging.Error(err)
+				return Result{
+					Name:    a.Underlying.GetName(),
+					Success: false,
+					Error:   err.Error(),
+				}
+			}
 		}
 	}
 	return Result{
diff --git a/pkg/crc/api/api.go b/pkg/crc/api/api.go
index 220206e08..7b7953b37 100644
--- a/pkg/crc/api/api.go
+++ b/pkg/crc/api/api.go
@@ -11,22 +11,23 @@ import (
 	"github.com/code-ready/crc/pkg/crc/machine"
 )
 
-func CreateServer(socketPath string, config crcConfig.Storage, machine machine.Client) (Server, error) {
+func CreateServer(socketPath string, config crcConfig.Storage, machine machine.Client, logger Logger) (Server, error) {
 	listener, err := net.Listen("unix", socketPath)
 	if err != nil {
 		logging.Error("Failed to create socket: ", err.Error())
 		return Server{}, err
 	}
-	return createServerWithListener(listener, config, machine)
+	return createServerWithListener(listener, config, machine, logger)
 }
 
-func createServerWithListener(listener net.Listener, config crcConfig.Storage, machine machine.Client) (Server, error) {
+func createServerWithListener(listener net.Listener, config crcConfig.Storage, machine machine.Client, logger Logger) (Server, error) {
 	apiServer := Server{
 		listener:               listener,
 		clusterOpsRequestsChan: make(chan clusterOpsRequest, 10),
 		handler: &Handler{
 			Config:        config,
 			MachineClient: &Adapter{Underlying: machine},
+			Logger:        logger,
 		},
 	}
 	return apiServer, nil
@@ -76,6 +77,8 @@ func (api Server) handleRequest(req commandRequest, conn net.Conn) {
 		result = api.handler.GetConfig(req.Args)
 	case "webconsoleurl":
 		result = api.handler.GetWebconsoleInfo()
+	case "logs":
+		result = api.handler.Logs()
 	default:
 		result = encodeErrorToJSON(fmt.Sprintf("Unknown command supplied: %s", req.Command))
 	}
@@ -114,7 +117,7 @@ func (api Server) handleConnections(conn net.Conn) {
 			conn.Close()
 		}
 
-	case "status", "version", "setconfig", "getconfig", "unsetconfig", "webconsoleurl":
+	case "status", "version", "setconfig", "getconfig", "unsetconfig", "webconsoleurl", "logs":
 		go api.handleRequest(req, conn)
 
 	default:
diff --git a/pkg/crc/api/handlers.go b/pkg/crc/api/handlers.go
index 8684e905f..5eb57df20 100644
--- a/pkg/crc/api/handlers.go
+++ b/pkg/crc/api/handlers.go
@@ -17,10 +17,22 @@ import (
 )
 
 type Handler struct {
+	Logger        Logger
 	MachineClient AdaptedClient
 	Config        crcConfig.Storage
 }
 
+type Logger interface {
+	Messages() []string
+}
+
+func (h *Handler) Logs() string {
+	return encodeStructToJSON(&loggerResult{
+		Success:  true,
+		Messages: h.Logger.Messages(),
+	})
+}
+
 func (h *Handler) Status() string {
 	clusterStatus := h.MachineClient.Status()
 	return encodeStructToJSON(clusterStatus)
diff --git a/pkg/crc/api/types.go b/pkg/crc/api/types.go
index f68e13b83..4909dfc29 100644
--- a/pkg/crc/api/types.go
+++ b/pkg/crc/api/types.go
@@ -21,6 +21,7 @@ type RequestHandler interface {
 	UnsetConfig(json.RawMessage) string
 	GetConfig(json.RawMessage) string
 	GetWebconsoleInfo() string
+	Logs() string
 }
 
 // clusterOpsRequest struct is used to store the command request and associated socket
@@ -52,3 +53,8 @@ type getConfigResult struct {
 type startArgs struct {
 	PullSecretFile string `json:"pullSecretFile"`
 }
+
+type loggerResult struct {
+	Success  bool
+	Messages []string
+}
diff --git a/pkg/crc/logging/inmemory.go b/pkg/crc/logging/inmemory.go
new file mode 100644
index 000000000..756014739
--- /dev/null
+++ b/pkg/crc/logging/inmemory.go
@@ -0,0 +1,38 @@
+package logging
+
+import (
+	"container/ring"
+
+	"github.com/sirupsen/logrus"
+)
+
+// This hook keeps in memory n messages from error to info level
+type inMemoryHook struct {
+	messages *ring.Ring
+}
+
+func newInMemoryHook(size int) *inMemoryHook {
+	return &inMemoryHook{
+		messages: ring.New(size),
+	}
+}
+
+func (h *inMemoryHook) Levels() []logrus.Level {
+	return []logrus.Level{logrus.InfoLevel, logrus.WarnLevel, logrus.ErrorLevel}
+}
+
+func (h *inMemoryHook) Fire(entry *logrus.Entry) error {
+	h.messages.Value = entry.Message
+	h.messages = h.messages.Next()
+	return nil
+}
+
+func (h *inMemoryHook) Messages() []string {
+	var ret []string
+	h.messages.Do(func(elem interface{}) {
+		if str, ok := elem.(string); ok {
+			ret = append(ret, str)
+		}
+	})
+	return ret
+}
diff --git a/pkg/crc/logging/logging.go b/pkg/crc/logging/logging.go
index 370730795..8645dc33d 100644
--- a/pkg/crc/logging/logging.go
+++ b/pkg/crc/logging/logging.go
@@ -14,6 +14,7 @@ var (
 	logfile       *os.File
 	LogLevel      string
 	originalHooks = logrus.LevelHooks{}
+	Memory        = newInMemoryHook(100)
 )
 
 func OpenLogFile(path string) (*os.File, error) {
@@ -53,6 +54,8 @@ func InitLogrus(logLevel, logFilePath string) {
 		level = logrus.InfoLevel
 	}
 
+	logrus.AddHook(Memory)
+
 	// Add hook to send error/fatal to stderr
 	logrus.AddHook(newstdErrHook(level, &logrus.TextFormatter{
 		ForceColors:            terminal.IsTerminal(int(os.Stderr.Fd())),


Test patch:
diff --git a/pkg/crc/api/api_test.go b/pkg/crc/api/api_test.go
index e97f8119a..76822ad87 100644
--- a/pkg/crc/api/api_test.go
+++ b/pkg/crc/api/api_test.go
@@ -31,7 +31,7 @@ func TestApi(t *testing.T) {
 	require.NoError(t, err)
 
 	client := fakemachine.NewClient()
-	api, err := createServerWithListener(listener, setupNewInMemoryConfig(), client)
+	api, err := createServerWithListener(listener, setupNewInMemoryConfig(), client, &mockLogger{})
 	require.NoError(t, err)
 	go func() {
 		if err := api.Serve(); err != nil {
@@ -118,6 +118,13 @@ func TestApi(t *testing.T) {
 				},
 			},
 		},
+		{
+			command: "logs",
+			expected: map[string]interface{}{
+				"Messages": []interface{}{"message 1", "message 2", "message 3"},
+				"Success":  true,
+			},
+		},
 	}
 	for _, test := range tt {
 		client.Failing = test.clientFailing
@@ -218,7 +225,7 @@ func setupAPIServer(t *testing.T) (string, func()) {
 	require.NoError(t, err)
 
 	client := fakemachine.NewClient()
-	api, err := createServerWithListener(listener, setupNewInMemoryConfig(), client)
+	api, err := createServerWithListener(listener, setupNewInMemoryConfig(), client, &mockLogger{})
 	require.NoError(t, err)
 	go func() {
 		if err := api.Serve(); err != nil {
@@ -247,3 +254,10 @@ func (s *skipPreflights) Set(key string, value interface{}) error {
 func (s *skipPreflights) Unset(key string) error {
 	return s.storage.Unset(key)
 }
+
+type mockLogger struct {
+}
+
+func (*mockLogger) Messages() []string {
+	return []string{"message 1", "message 2", "message 3"}
+}
diff --git a/pkg/crc/logging/inmemory_test.go b/pkg/crc/logging/inmemory_test.go
new file mode 100644
index 000000000..3061878aa
--- /dev/null
+++ b/pkg/crc/logging/inmemory_test.go
@@ -0,0 +1,23 @@
+package logging
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAddLogs(t *testing.T) {
+	memory := newInMemoryHook(5)
+
+	assert.Len(t, memory.Messages(), 0)
+
+	for i := 1; i < 10; i++ {
+		assert.NoError(t, memory.Fire(&logrus.Entry{
+			Message: fmt.Sprintf("message %d", i),
+		}))
+	}
+
+	assert.Equal(t, []string{"message 5", "message 6", "message 7", "message 8", "message 9"}, memory.Messages())
+}