From 6ae8fabdf6723e4e1302352dfecb32100b90abde Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Tue, 22 Oct 2024 17:16:35 +0200 Subject: [PATCH 01/17] Support the addr kw in pool.drbd Relay the addr@ definitions to the disk.drbd resources generated by the pool. --- drivers/pooldrbd/main.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/drivers/pooldrbd/main.go b/drivers/pooldrbd/main.go index f7c8e547b..370ee3ba8 100644 --- a/drivers/pooldrbd/main.go +++ b/drivers/pooldrbd/main.go @@ -6,6 +6,7 @@ import ( "fmt" "path/filepath" + "github.com/opensvc/om3/core/cluster" "github.com/opensvc/om3/core/driver" "github.com/opensvc/om3/core/pool" "github.com/opensvc/om3/core/rawconfig" @@ -60,6 +61,17 @@ func (t T) network() string { return t.GetString("network") } +func (t T) addrs() map[string]string { + m := make(map[string]string) + for _, nodename := range cluster.ConfigData.Get().Nodes { + s := t.GetStringAs("addr", nodename) + if s != "" { + m[nodename] = s + } + } + return m +} + func (t T) path() string { if p := t.GetString("path"); p != "" { return p @@ -228,6 +240,9 @@ func (t *T) commonDrbdKeywords(rid string) (l []string) { if network != "" { l = append(l, rid+".network="+network) } + for nodename, addr := range t.addrs() { + l = append(l, rid+".addr@"+nodename+"="+addr) + } return } From 03fcf25a0bc13aab2e8fa0c3603db1f72f994dcd Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Tue, 22 Oct 2024 17:18:06 +0200 Subject: [PATCH 02/17] Add pool.drbd addr kw doc strings --- core/object/text/kw/node/pool.drbd.addr | 2 ++ core/object/text/kw/node/pool.drbd.addr.default | 1 + 2 files changed, 3 insertions(+) create mode 100644 core/object/text/kw/node/pool.drbd.addr create mode 100644 core/object/text/kw/node/pool.drbd.addr.default diff --git a/core/object/text/kw/node/pool.drbd.addr b/core/object/text/kw/node/pool.drbd.addr new file mode 100644 index 000000000..f6ee8bd63 --- /dev/null +++ b/core/object/text/kw/node/pool.drbd.addr @@ -0,0 +1,2 @@ +The addr to use to connect a peer. Use scoping to define each non-default +address. diff --git a/core/object/text/kw/node/pool.drbd.addr.default b/core/object/text/kw/node/pool.drbd.addr.default new file mode 100644 index 000000000..40ab9ce27 --- /dev/null +++ b/core/object/text/kw/node/pool.drbd.addr.default @@ -0,0 +1 @@ +The ipaddr resolved for the nodename. From 56a9c5f7abb2d5e8e1b454ef85af532a6c74218c Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Tue, 22 Oct 2024 17:20:09 +0200 Subject: [PATCH 03/17] Add the POST /node/name/.../instance/.../action/restart api handler --- daemon/api/api.yaml | 38 ++ daemon/api/codegen_client_gen.go | 280 ++++++++++ daemon/api/codegen_server_gen.go | 515 +++++++++++------- daemon/api/codegen_type_gen.go | 11 + .../daemonapi/post_instance_action_restart.go | 63 +++ 5 files changed, 699 insertions(+), 208 deletions(-) create mode 100644 daemon/daemonapi/post_instance_action_restart.go diff --git a/daemon/api/api.yaml b/daemon/api/api.yaml index c7fd5aa58..d33d5def5 100644 --- a/daemon/api/api.yaml +++ b/daemon/api/api.yaml @@ -1390,6 +1390,44 @@ paths: - instance / svc - instance / vol + /node/name/{nodename}/instance/path/{namespace}/{kind}/{name}/action/restart: + post: + description: Restart the object instance. + operationId: PostInstanceActionRestart + parameters: + - $ref: '#/components/parameters/inPathNodeName' + - $ref: '#/components/parameters/inPathNamespace' + - $ref: '#/components/parameters/inPathKind' + - $ref: '#/components/parameters/inPathName' + - $ref: '#/components/parameters/inQueryDisableRollback' + - $ref: '#/components/parameters/inQueryForce' + - $ref: '#/components/parameters/inQueryRequesterSid' + - $ref: '#/components/parameters/inQueryRid' + - $ref: '#/components/parameters/inQuerySubset' + - $ref: '#/components/parameters/inQueryTag' + - $ref: '#/components/parameters/inQueryTo' + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/InstanceActionAccepted' + description: OK + 400: + $ref: '#/components/responses/400' + 401: + $ref: '#/components/responses/401' + 403: + $ref: '#/components/responses/403' + 500: + $ref: '#/components/responses/500' + security: + - basicAuth: [] + - bearerAuth: [] + tags: + - instance / svc + - instance / vol + /node/name/{nodename}/instance/path/{namespace}/{kind}/{name}/action/start: post: description: Start the object instance. diff --git a/daemon/api/codegen_client_gen.go b/daemon/api/codegen_client_gen.go index d73b4ae5f..ce244486d 100644 --- a/daemon/api/codegen_client_gen.go +++ b/daemon/api/codegen_client_gen.go @@ -243,6 +243,9 @@ type ClientInterface interface { // PostInstanceActionPRStop request PostInstanceActionPRStop(ctx context.Context, nodename InPathNodeName, namespace InPathNamespace, kind InPathKind, name InPathName, params *PostInstanceActionPRStopParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // PostInstanceActionRestart request + PostInstanceActionRestart(ctx context.Context, nodename InPathNodeName, namespace InPathNamespace, kind InPathKind, name InPathName, params *PostInstanceActionRestartParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // PostInstanceActionShutdown request PostInstanceActionShutdown(ctx context.Context, nodename InPathNodeName, namespace InPathNamespace, kind InPathKind, name InPathName, params *PostInstanceActionShutdownParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1086,6 +1089,18 @@ func (c *Client) PostInstanceActionPRStop(ctx context.Context, nodename InPathNo return c.Client.Do(req) } +func (c *Client) PostInstanceActionRestart(ctx context.Context, nodename InPathNodeName, namespace InPathNamespace, kind InPathKind, name InPathName, params *PostInstanceActionRestartParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostInstanceActionRestartRequest(c.Server, nodename, namespace, kind, name, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) PostInstanceActionShutdown(ctx context.Context, nodename InPathNodeName, namespace InPathNamespace, kind InPathKind, name InPathName, params *PostInstanceActionShutdownParams, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewPostInstanceActionShutdownRequest(c.Server, nodename, namespace, kind, name, params) if err != nil { @@ -4861,6 +4876,179 @@ func NewPostInstanceActionPRStopRequest(server string, nodename InPathNodeName, return req, nil } +// NewPostInstanceActionRestartRequest generates requests for PostInstanceActionRestart +func NewPostInstanceActionRestartRequest(server string, nodename InPathNodeName, namespace InPathNamespace, kind InPathKind, name InPathName, params *PostInstanceActionRestartParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "nodename", runtime.ParamLocationPath, nodename) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "namespace", runtime.ParamLocationPath, namespace) + if err != nil { + return nil, err + } + + var pathParam2 string + + pathParam2, err = runtime.StyleParamWithLocation("simple", false, "kind", runtime.ParamLocationPath, kind) + if err != nil { + return nil, err + } + + var pathParam3 string + + pathParam3, err = runtime.StyleParamWithLocation("simple", false, "name", runtime.ParamLocationPath, name) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/node/name/%s/instance/path/%s/%s/%s/action/restart", pathParam0, pathParam1, pathParam2, pathParam3) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.DisableRollback != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "disable_rollback", runtime.ParamLocationQuery, *params.DisableRollback); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Force != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "force", runtime.ParamLocationQuery, *params.Force); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.RequesterSid != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "requester_sid", runtime.ParamLocationQuery, *params.RequesterSid); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Rid != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "rid", runtime.ParamLocationQuery, *params.Rid); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Subset != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "subset", runtime.ParamLocationQuery, *params.Subset); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Tag != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "tag", runtime.ParamLocationQuery, *params.Tag); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.To != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "to", runtime.ParamLocationQuery, *params.To); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("POST", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewPostInstanceActionShutdownRequest generates requests for PostInstanceActionShutdown func NewPostInstanceActionShutdownRequest(server string, nodename InPathNodeName, namespace InPathNamespace, kind InPathKind, name InPathName, params *PostInstanceActionShutdownParams) (*http.Request, error) { var err error @@ -9269,6 +9457,9 @@ type ClientWithResponsesInterface interface { // PostInstanceActionPRStopWithResponse request PostInstanceActionPRStopWithResponse(ctx context.Context, nodename InPathNodeName, namespace InPathNamespace, kind InPathKind, name InPathName, params *PostInstanceActionPRStopParams, reqEditors ...RequestEditorFn) (*PostInstanceActionPRStopResponse, error) + // PostInstanceActionRestartWithResponse request + PostInstanceActionRestartWithResponse(ctx context.Context, nodename InPathNodeName, namespace InPathNamespace, kind InPathKind, name InPathName, params *PostInstanceActionRestartParams, reqEditors ...RequestEditorFn) (*PostInstanceActionRestartResponse, error) + // PostInstanceActionShutdownWithResponse request PostInstanceActionShutdownWithResponse(ctx context.Context, nodename InPathNodeName, namespace InPathNamespace, kind InPathKind, name InPathName, params *PostInstanceActionShutdownParams, reqEditors ...RequestEditorFn) (*PostInstanceActionShutdownResponse, error) @@ -10711,6 +10902,32 @@ func (r PostInstanceActionPRStopResponse) StatusCode() int { return 0 } +type PostInstanceActionRestartResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *InstanceActionAccepted + JSON400 *N400 + JSON401 *N401 + JSON403 *N403 + JSON500 *N500 +} + +// Status returns HTTPResponse.Status +func (r PostInstanceActionRestartResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PostInstanceActionRestartResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type PostInstanceActionShutdownResponse struct { Body []byte HTTPResponse *http.Response @@ -12918,6 +13135,15 @@ func (c *ClientWithResponses) PostInstanceActionPRStopWithResponse(ctx context.C return ParsePostInstanceActionPRStopResponse(rsp) } +// PostInstanceActionRestartWithResponse request returning *PostInstanceActionRestartResponse +func (c *ClientWithResponses) PostInstanceActionRestartWithResponse(ctx context.Context, nodename InPathNodeName, namespace InPathNamespace, kind InPathKind, name InPathName, params *PostInstanceActionRestartParams, reqEditors ...RequestEditorFn) (*PostInstanceActionRestartResponse, error) { + rsp, err := c.PostInstanceActionRestart(ctx, nodename, namespace, kind, name, params, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostInstanceActionRestartResponse(rsp) +} + // PostInstanceActionShutdownWithResponse request returning *PostInstanceActionShutdownResponse func (c *ClientWithResponses) PostInstanceActionShutdownWithResponse(ctx context.Context, nodename InPathNodeName, namespace InPathNamespace, kind InPathKind, name InPathName, params *PostInstanceActionShutdownParams, reqEditors ...RequestEditorFn) (*PostInstanceActionShutdownResponse, error) { rsp, err := c.PostInstanceActionShutdown(ctx, nodename, namespace, kind, name, params, reqEditors...) @@ -16045,6 +16271,60 @@ func ParsePostInstanceActionPRStopResponse(rsp *http.Response) (*PostInstanceAct return response, nil } +// ParsePostInstanceActionRestartResponse parses an HTTP response from a PostInstanceActionRestartWithResponse call +func ParsePostInstanceActionRestartResponse(rsp *http.Response) (*PostInstanceActionRestartResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PostInstanceActionRestartResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest InstanceActionAccepted + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest N400 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest N401 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest N403 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest N500 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParsePostInstanceActionShutdownResponse parses an HTTP response from a PostInstanceActionShutdownWithResponse call func ParsePostInstanceActionShutdownResponse(rsp *http.Response) (*PostInstanceActionShutdownResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/daemon/api/codegen_server_gen.go b/daemon/api/codegen_server_gen.go index 44e1da327..5e893fb6d 100644 --- a/daemon/api/codegen_server_gen.go +++ b/daemon/api/codegen_server_gen.go @@ -165,6 +165,9 @@ type ServerInterface interface { // (POST /node/name/{nodename}/instance/path/{namespace}/{kind}/{name}/action/prstop) PostInstanceActionPRStop(ctx echo.Context, nodename InPathNodeName, namespace InPathNamespace, kind InPathKind, name InPathName, params PostInstanceActionPRStopParams) error + // (POST /node/name/{nodename}/instance/path/{namespace}/{kind}/{name}/action/restart) + PostInstanceActionRestart(ctx echo.Context, nodename InPathNodeName, namespace InPathNamespace, kind InPathKind, name InPathName, params PostInstanceActionRestartParams) error + // (POST /node/name/{nodename}/instance/path/{namespace}/{kind}/{name}/action/shutdown) PostInstanceActionShutdown(ctx echo.Context, nodename InPathNodeName, namespace InPathNamespace, kind InPathKind, name InPathName, params PostInstanceActionShutdownParams) error @@ -1912,6 +1915,101 @@ func (w *ServerInterfaceWrapper) PostInstanceActionPRStop(ctx echo.Context) erro return err } +// PostInstanceActionRestart converts echo context to params. +func (w *ServerInterfaceWrapper) PostInstanceActionRestart(ctx echo.Context) error { + var err error + // ------------- Path parameter "nodename" ------------- + var nodename InPathNodeName + + err = runtime.BindStyledParameterWithLocation("simple", false, "nodename", runtime.ParamLocationPath, ctx.Param("nodename"), &nodename) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter nodename: %s", err)) + } + + // ------------- Path parameter "namespace" ------------- + var namespace InPathNamespace + + err = runtime.BindStyledParameterWithLocation("simple", false, "namespace", runtime.ParamLocationPath, ctx.Param("namespace"), &namespace) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter namespace: %s", err)) + } + + // ------------- Path parameter "kind" ------------- + var kind InPathKind + + err = runtime.BindStyledParameterWithLocation("simple", false, "kind", runtime.ParamLocationPath, ctx.Param("kind"), &kind) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter kind: %s", err)) + } + + // ------------- Path parameter "name" ------------- + var name InPathName + + err = runtime.BindStyledParameterWithLocation("simple", false, "name", runtime.ParamLocationPath, ctx.Param("name"), &name) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter name: %s", err)) + } + + ctx.Set(BasicAuthScopes, []string{}) + + ctx.Set(BearerAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params PostInstanceActionRestartParams + // ------------- Optional query parameter "disable_rollback" ------------- + + err = runtime.BindQueryParameter("form", true, false, "disable_rollback", ctx.QueryParams(), ¶ms.DisableRollback) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter disable_rollback: %s", err)) + } + + // ------------- Optional query parameter "force" ------------- + + err = runtime.BindQueryParameter("form", true, false, "force", ctx.QueryParams(), ¶ms.Force) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter force: %s", err)) + } + + // ------------- Optional query parameter "requester_sid" ------------- + + err = runtime.BindQueryParameter("form", true, false, "requester_sid", ctx.QueryParams(), ¶ms.RequesterSid) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter requester_sid: %s", err)) + } + + // ------------- Optional query parameter "rid" ------------- + + err = runtime.BindQueryParameter("form", true, false, "rid", ctx.QueryParams(), ¶ms.Rid) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter rid: %s", err)) + } + + // ------------- Optional query parameter "subset" ------------- + + err = runtime.BindQueryParameter("form", true, false, "subset", ctx.QueryParams(), ¶ms.Subset) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter subset: %s", err)) + } + + // ------------- Optional query parameter "tag" ------------- + + err = runtime.BindQueryParameter("form", true, false, "tag", ctx.QueryParams(), ¶ms.Tag) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter tag: %s", err)) + } + + // ------------- Optional query parameter "to" ------------- + + err = runtime.BindQueryParameter("form", true, false, "to", ctx.QueryParams(), ¶ms.To) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter to: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.PostInstanceActionRestart(ctx, nodename, namespace, kind, name, params) + return err +} + // PostInstanceActionShutdown converts echo context to params. func (w *ServerInterfaceWrapper) PostInstanceActionShutdown(ctx echo.Context) error { var err error @@ -4573,6 +4671,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.POST(baseURL+"/node/name/:nodename/instance/path/:namespace/:kind/:name/action/provision", wrapper.PostInstanceActionProvision) router.POST(baseURL+"/node/name/:nodename/instance/path/:namespace/:kind/:name/action/prstart", wrapper.PostInstanceActionPRStart) router.POST(baseURL+"/node/name/:nodename/instance/path/:namespace/:kind/:name/action/prstop", wrapper.PostInstanceActionPRStop) + router.POST(baseURL+"/node/name/:nodename/instance/path/:namespace/:kind/:name/action/restart", wrapper.PostInstanceActionRestart) router.POST(baseURL+"/node/name/:nodename/instance/path/:namespace/:kind/:name/action/shutdown", wrapper.PostInstanceActionShutdown) router.POST(baseURL+"/node/name/:nodename/instance/path/:namespace/:kind/:name/action/start", wrapper.PostInstanceActionStart) router.POST(baseURL+"/node/name/:nodename/instance/path/:namespace/:kind/:name/action/startstandby", wrapper.PostInstanceActionStartStandby) @@ -4645,214 +4744,214 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "/z8AAP//vQXjhKjrAQA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/daemon/api/codegen_type_gen.go b/daemon/api/codegen_type_gen.go index 2220843ff..0f3733551 100644 --- a/daemon/api/codegen_type_gen.go +++ b/daemon/api/codegen_type_gen.go @@ -1774,6 +1774,17 @@ type PostInstanceActionPRStopParams struct { To *InQueryTo `form:"to,omitempty" json:"to,omitempty"` } +// PostInstanceActionRestartParams defines parameters for PostInstanceActionRestart. +type PostInstanceActionRestartParams struct { + DisableRollback *InQueryDisableRollback `form:"disable_rollback,omitempty" json:"disable_rollback,omitempty"` + Force *InQueryForce `form:"force,omitempty" json:"force,omitempty"` + RequesterSid *InQueryRequesterSid `form:"requester_sid,omitempty" json:"requester_sid,omitempty"` + Rid *InQueryRid `form:"rid,omitempty" json:"rid,omitempty"` + Subset *InQuerySubset `form:"subset,omitempty" json:"subset,omitempty"` + Tag *InQueryTag `form:"tag,omitempty" json:"tag,omitempty"` + To *InQueryTo `form:"to,omitempty" json:"to,omitempty"` +} + // PostInstanceActionShutdownParams defines parameters for PostInstanceActionShutdown. type PostInstanceActionShutdownParams struct { Force *InQueryForce `form:"force,omitempty" json:"force,omitempty"` diff --git a/daemon/daemonapi/post_instance_action_restart.go b/daemon/daemonapi/post_instance_action_restart.go new file mode 100644 index 000000000..44b20c864 --- /dev/null +++ b/daemon/daemonapi/post_instance_action_restart.go @@ -0,0 +1,63 @@ +package daemonapi + +import ( + "net/http" + + "github.com/google/uuid" + "github.com/labstack/echo/v4" + + "github.com/opensvc/om3/core/client" + "github.com/opensvc/om3/core/naming" + "github.com/opensvc/om3/daemon/api" + "github.com/opensvc/om3/daemon/rbac" +) + +func (a *DaemonAPI) PostInstanceActionRestart(ctx echo.Context, nodename, namespace string, kind naming.Kind, name string, params api.PostInstanceActionRestartParams) error { + if a.localhost == nodename { + return a.postLocalInstanceActionRestart(ctx, namespace, kind, name, params) + } + return a.proxy(ctx, nodename, func(c *client.T) (*http.Response, error) { + return c.PostInstanceActionRestart(ctx.Request().Context(), nodename, namespace, kind, name, ¶ms) + }) +} + +func (a *DaemonAPI) postLocalInstanceActionRestart(ctx echo.Context, namespace string, kind naming.Kind, name string, params api.PostInstanceActionRestartParams) error { + if v, err := assertGrant(ctx, rbac.NewGrant(rbac.RoleOperator, namespace), rbac.NewGrant(rbac.RoleAdmin, namespace), rbac.GrantRoot); !v { + return err + } + + log := LogHandler(ctx, "PostInstanceActionRestart") + var requesterSid uuid.UUID + p, err := naming.NewPath(namespace, kind, name) + if err != nil { + return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameters", "%s", err) + } + log = naming.LogWithPath(log, p) + args := []string{p.String(), "restart", "--local"} + if params.DisableRollback != nil && *params.DisableRollback { + args = append(args, "--disable-rollback") + } + if params.Force != nil && *params.Force { + args = append(args, "--force") + } + if params.To != nil && *params.To != "" { + args = append(args, "--to", *params.To) + } + if params.Rid != nil && *params.Rid != "" { + args = append(args, "--rid", *params.Rid) + } + if params.Subset != nil && *params.Subset != "" { + args = append(args, "--subset", *params.Subset) + } + if params.Tag != nil && *params.Tag != "" { + args = append(args, "--tag", *params.Tag) + } + if params.RequesterSid != nil { + requesterSid = *params.RequesterSid + } + if sid, err := a.apiExec(ctx, p, requesterSid, args, log); err != nil { + return JSONProblemf(ctx, http.StatusInternalServerError, "", "%s", err) + } else { + return ctx.JSON(http.StatusOK, api.InstanceActionAccepted{SessionID: sid}) + } +} From 493308ac076bc1e5e545ba8348e02fe4a71e0698 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Tue, 22 Oct 2024 17:21:05 +0200 Subject: [PATCH 04/17] Switch to hicolors the frozen icon was unreadable on black bg --- util/render/palette/palette.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/util/render/palette/palette.go b/util/render/palette/palette.go index 112787b38..417b7ec44 100644 --- a/util/render/palette/palette.go +++ b/util/render/palette/palette.go @@ -6,9 +6,9 @@ import "github.com/fatih/color" const ( DefaultPrimary = "himagenta" DefaultSecondary = "hiblack" - DefaultOptimal = "green" - DefaultError = "red" - DefaultWarning = "yellow" + DefaultOptimal = "higreen" + DefaultError = "hired" + DefaultWarning = "hiyellow" DefaultFrozen = "hiblue" ) From b1adf3e5b35540298e21268481203c65a678fb03 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Tue, 22 Oct 2024 17:22:11 +0200 Subject: [PATCH 05/17] Add the pool.drbd kw in the node and cluster dictionnaries --- core/object/node_keywords.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/object/node_keywords.go b/core/object/node_keywords.go index a0f9a5052..567842504 100644 --- a/core/object/node_keywords.go +++ b/core/object/node_keywords.go @@ -889,6 +889,13 @@ var nodeCommonKeywords = []keywords.Keyword{ Text: keywords.NewText(fs, "text/kw/node/pool.vg.name"), Types: []string{"vg"}, }, + { + DefaultText: keywords.NewText(fs, "text/kw/node/pool.drbd.addr.default"), + Example: "1.2.3.4", + Option: "addr", + Scopable: true, + Text: keywords.NewText(fs, "text/kw/node/pool.drbd.addr"), + }, { Option: "vg", Section: "pool", From d856168742473158dff0e33603fb33e90834b972 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Tue, 22 Oct 2024 17:23:33 +0200 Subject: [PATCH 06/17] Fix ox node logs trying to access directly the endpoint --- core/oxcmd/node_logs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/oxcmd/node_logs.go b/core/oxcmd/node_logs.go index 9015b4c66..7cb70c6bf 100644 --- a/core/oxcmd/node_logs.go +++ b/core/oxcmd/node_logs.go @@ -22,7 +22,7 @@ type ( ) func (t *CmdNodeLogs) stream(node string) { - c, err := client.New(client.WithURL(node), client.WithTimeout(0)) + c, err := client.New(client.WithURL(t.Server), client.WithTimeout(0)) if err != nil { fmt.Fprintln(os.Stderr, err) return From 9f300c816331e5f05d4778698422490ca2741f59 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Tue, 22 Oct 2024 17:25:08 +0200 Subject: [PATCH 07/17] Make some monitor.Frame renderers public so they can be used by the 'ot' program. --- core/monitor/frame.go | 22 ++++++++++-------- core/monitor/frame_instance.go | 4 ++-- core/monitor/frame_nodes.go | 42 +++++++++++++++++++--------------- core/monitor/frame_objects.go | 24 ++++++++++++------- core/monitor/frame_threads.go | 6 ++--- 5 files changed, 56 insertions(+), 42 deletions(-) diff --git a/core/monitor/frame.go b/core/monitor/frame.go index 86cde65e5..0227ee8ed 100644 --- a/core/monitor/frame.go +++ b/core/monitor/frame.go @@ -26,14 +26,15 @@ var ( "objects": sectionObjects, "services": sectionObjects, } - green, yellow, red, blue, hiblue, hiblack, bold func(a ...interface{}) string + green, yellow, hired, red, blue, hiblue, hiblack, bold func(a ...interface{}) string iconUp, iconWarning, iconDownIssue, iconPlacementAlert, iconProvisionAlert, iconStandbyDown, iconStandbyUpIssue, iconUndef, iconFrozen, iconDown, iconDRP, iconLeader, iconNotApplicable, iconPreserved, iconStandbyUp string ) -func initColor() { +func InitColor() { green = color.New(color.FgGreen).SprintFunc() yellow = color.New(color.FgYellow).SprintFunc() red = color.New(color.FgRed).SprintFunc() + hired = color.New(color.FgHiRed).SprintFunc() blue = color.New(color.FgBlue).SprintFunc() hiblue = color.New(color.FgHiBlue).SprintFunc() hiblack = color.New(color.FgHiBlack).SprintFunc() @@ -41,13 +42,13 @@ func initColor() { iconUp = green("O") iconWarning = yellow("!") - iconDownIssue = red("X") - iconPlacementAlert = red("^") - iconProvisionAlert = red("P") - iconStandbyDown = red("x") - iconStandbyUpIssue = red("o") - iconUndef = red("?") - iconFrozen = blue("*") + iconDownIssue = hired("X") + iconPlacementAlert = hired("^") + iconProvisionAlert = hired("P") + iconStandbyDown = hired("x") + iconStandbyUpIssue = hired("o") + iconUndef = hired("?") + iconFrozen = hiblue("*") iconDown = hiblack("X") iconDRP = hiblack("#") iconLeader = hiblack("^") @@ -103,11 +104,12 @@ func (f Frame) hasSection(section string) bool { // representation of Render. func (f *Frame) Render() string { var builder strings.Builder - initColor() + InitColor() green = color.New(color.FgGreen).SprintFunc() yellow = color.New(color.FgYellow).SprintFunc() red = color.New(color.FgRed).SprintFunc() + hired = color.New(color.FgHiRed).SprintFunc() blue = color.New(color.FgBlue).SprintFunc() hiblue = color.New(color.FgHiBlue).SprintFunc() hiblack = color.New(color.FgHiBlack).SprintFunc() diff --git a/core/monitor/frame_instance.go b/core/monitor/frame_instance.go index 896adb448..bb6e2a0f8 100644 --- a/core/monitor/frame_instance.go +++ b/core/monitor/frame_instance.go @@ -8,7 +8,7 @@ import ( "github.com/opensvc/om3/core/status" ) -func (f Frame) sObjectInstance(path string, node string, scope []string) string { +func (f Frame) StrObjectInstance(path string, node string, scope []string) string { s := "" avail := f.Current.Cluster.Object[path].Avail inst := f.Current.Cluster.Node[node].Instance[path] @@ -37,7 +37,7 @@ func (f Frame) sObjectInstance(path string, node string, scope []string) string } else if inst.Config != nil || slices.Contains(scope, node) { s += iconUndef } - return s + "\t" + return s } func sObjectInstanceAvail(objectAvail status.T, instance instance.Status, mon instance.Monitor) string { diff --git a/core/monitor/frame_nodes.go b/core/monitor/frame_nodes.go index ff44cab9d..6c57dac48 100644 --- a/core/monitor/frame_nodes.go +++ b/core/monitor/frame_nodes.go @@ -13,14 +13,14 @@ import ( func (f Frame) sNodeScoreLine() string { s := fmt.Sprintf(" %s\t\t\t%s\t", bold("score"), f.info.separator) for _, n := range f.Current.Cluster.Config.Nodes { - s += f.sNodeScore(n) + "\t" + s += f.StrNodeScore(n) + "\t" } return s } func (f Frame) sNodeLoadLine() string { s := fmt.Sprintf(" %s\t\t\t%s\t", bold("load15m"), f.info.separator) for _, n := range f.Current.Cluster.Config.Nodes { - s += f.sNodeLoad(n) + "\t" + s += f.StrNodeLoad(n) + "\t" } return s } @@ -28,7 +28,7 @@ func (f Frame) sNodeLoadLine() string { func (f Frame) sNodeMemLine() string { s := fmt.Sprintf(" %s\t\t\t%s\t", bold("mem"), f.info.separator) for _, n := range f.Current.Cluster.Config.Nodes { - s += f.sNodeMem(n) + "\t" + s += f.StrNodeMem(n) + "\t" } return s } @@ -36,18 +36,22 @@ func (f Frame) sNodeMemLine() string { func (f Frame) sNodeSwapLine() string { s := fmt.Sprintf(" %s\t\t\t%s\t", bold("swap"), f.info.separator) for _, n := range f.Current.Cluster.Config.Nodes { - s += f.sNodeSwap(n) + "\t" + s += f.StrNodeSwap(n) + "\t" } return s } +func (f Frame) StrNodeStates(n string) string { + s := f.sNodeMonState(n) + s += f.sNodeFrozen(n) + s += f.sNodeMonTarget(n) + return s +} + func (f Frame) sNodeWarningsLine() string { s := fmt.Sprintf(" %s\t\t\t%s\t", bold("state"), f.info.separator) for _, n := range f.Current.Cluster.Config.Nodes { - s += f.sNodeMonState(n) - s += f.sNodeFrozen(n) - s += f.sNodeMonTarget(n) - s += "\t" + s += f.StrNodeStates(n) + "\t" } return s } @@ -78,21 +82,21 @@ func (f Frame) sNodeCompatLine() string { return s + "\n" } -func (f Frame) sNodeScore(n string) string { +func (f Frame) StrNodeScore(n string) string { if val, ok := f.Current.Cluster.Node[n]; ok { return fmt.Sprintf("%d", val.Stats.Score) } return iconUndef } -func (f Frame) sNodeLoad(n string) string { +func (f Frame) StrNodeLoad(n string) string { if val, ok := f.Current.Cluster.Node[n]; ok { return fmt.Sprintf("%.1f", val.Stats.Load15M) } return iconUndef } -func (f Frame) sNodeMem(n string) string { +func (f Frame) StrNodeMem(n string) string { if val, ok := f.Current.Cluster.Node[n]; ok { if val.Stats.MemTotalMB == 0 { return hiblue("-") @@ -105,19 +109,19 @@ func (f Frame) sNodeMem(n string) string { total := sizeconv.BSizeCompactFromMB(val.Stats.MemTotalMB) var s string if limit > 0 { - s = fmt.Sprintf("%d/%d%%:%s", usage, limit, total) + s = fmt.Sprintf("%d%%%s<%d%%", usage, total, limit) } else { - s = fmt.Sprintf("%d%%:%s", usage, total) + s = fmt.Sprintf("%d%%%s", usage, total) } if usage > limit { - return red(s) + return hired(s) } return s } return iconUndef } -func (f Frame) sNodeSwap(n string) string { +func (f Frame) StrNodeSwap(n string) string { if val, ok := f.Current.Cluster.Node[n]; ok { if val.Stats.SwapTotalMB == 0 { return hiblue("-") @@ -130,12 +134,12 @@ func (f Frame) sNodeSwap(n string) string { total := sizeconv.BSizeCompactFromMB(val.Stats.SwapTotalMB) var s string if limit > 0 { - s = fmt.Sprintf("%d/%d%%:%s", usage, limit, total) + s = fmt.Sprintf("%d%%%s<%d%%", usage, total, limit) } else { - s = fmt.Sprintf("%d%%:%s", usage, total) + s = fmt.Sprintf("%d%%%s", usage, total) } if usage > limit { - return red(s) + return hired(s) } return s } @@ -220,7 +224,7 @@ func (f Frame) sNodeHbMode() string { } case "": if nodeCount > 1 { - mode = red("?") + mode = hired("?") } else { mode = "?" } diff --git a/core/monitor/frame_objects.go b/core/monitor/frame_objects.go index f95b85e68..0ef7c09d7 100644 --- a/core/monitor/frame_objects.go +++ b/core/monitor/frame_objects.go @@ -55,7 +55,12 @@ func (f Frame) scalerInstancesUp(path string) int { return actual } -func (f Frame) sObjectRunning(path string) string { +func (f Frame) sObjectOrchestrateAndRunning(path string) string { + s := f.Current.Cluster.Object[path] + return fmt.Sprintf("%-5s %s", s.Orchestrate, f.StrObjectRunning(path)) +} + +func (f Frame) StrObjectRunning(path string) string { var ( actual, expected int ) @@ -92,14 +97,18 @@ func (f Frame) sObjectRunning(path string) string { case actual == 0 && expected == 0: return "" case expected == 0: - return fmt.Sprintf("%-5s %d", s.Orchestrate, actual) + return fmt.Sprintf("%d", actual) case avail == status.NotApplicable: - return fmt.Sprintf("%-5s", s.Orchestrate) + return "" default: - return fmt.Sprintf("%-5s %d/%d", s.Orchestrate, actual, expected) + return fmt.Sprintf("%d/%d", actual, expected) } } +func StrObjectStatus(d object.Status) string { + return sObjectAvail(d) + sObjectWarning(d) + sObjectPlacement(d) +} + func sObjectAvail(d object.Status) string { s := d.Avail return colorstatus.Sprint(s, rawconfig.Colorize) @@ -107,13 +116,12 @@ func sObjectAvail(d object.Status) string { func (f Frame) sObject(path string) string { d := f.Current.Cluster.Object[path] - c3 := sObjectAvail(d) + sObjectWarning(d) + sObjectPlacement(d) s := fmt.Sprintf(" %s\t", bold(path)) - s += fmt.Sprintf("%s\t", c3) - s += fmt.Sprintf("%s\t", f.sObjectRunning(path)) + s += fmt.Sprintf("%s\t", StrObjectStatus(d)) + s += fmt.Sprintf("%s\t", f.sObjectOrchestrateAndRunning(path)) s += fmt.Sprintf("%s\t", f.info.separator) for _, node := range f.Current.Cluster.Config.Nodes { - s += f.sObjectInstance(path, node, d.Scope) + s += f.StrObjectInstance(path, node, d.Scope) + "\t" } return s } diff --git a/core/monitor/frame_threads.go b/core/monitor/frame_threads.go index f2793d168..f958d73fe 100644 --- a/core/monitor/frame_threads.go +++ b/core/monitor/frame_threads.go @@ -102,11 +102,11 @@ func (f Frame) wThreadHeartbeats() string { case "running": s += green("running") + sThreadAlerts(hbStatus.Alerts) case "stopped": - s += red("stopped") + sThreadAlerts(hbStatus.Alerts) + s += hired("stopped") + sThreadAlerts(hbStatus.Alerts) case "failed": - s += red("failed") + sThreadAlerts(hbStatus.Alerts) + s += hired("failed") + sThreadAlerts(hbStatus.Alerts) default: - s += red("unknown") + sThreadAlerts(hbStatus.Alerts) + s += hired("unknown") + sThreadAlerts(hbStatus.Alerts) } s += "\t" + hbStatus.Type + "\t" s += f.info.separator + "\t" From e6ad2847e413ca5f8ea005611b2e28b4d0ab0206 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Wed, 23 Oct 2024 08:40:38 +0200 Subject: [PATCH 08/17] Add the ot binary And the "ot" make target. --- Makefile | 6 +- cmd/ot/main.go | 1538 ++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 5 +- go.sum | 25 + 4 files changed, 1571 insertions(+), 3 deletions(-) create mode 100644 cmd/ot/main.go diff --git a/Makefile b/Makefile index 4a30c9dd1..dbdec0e6f 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ PREFIX ?= /usr DIST := dist OM := bin/om +OT := bin/ot OX := bin/ox COMPOBJ := bin/compobj COMPOBJ_D := share/opensvc/compliance @@ -25,7 +26,7 @@ VERSION := $(shell git describe --tags --abbrev) all: clean vet test race build dist -build: version api om ox compobj +build: version api om ot ox compobj deps: $(GOINSTALL) github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen@latest @@ -41,6 +42,9 @@ clean: om: $(GOBUILD) -o $(OM) ./cmd/om/ +ot: + $(GOBUILD) -o $(OT) ./cmd/ot/ + ox: $(GOBUILD) -o $(OX) ./cmd/ox/ diff --git a/cmd/ot/main.go b/cmd/ot/main.go new file mode 100644 index 000000000..16cdc6b56 --- /dev/null +++ b/cmd/ot/main.go @@ -0,0 +1,1538 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "sort" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/gdamore/tcell/v2" + "github.com/opensvc/om3/core/client" + "github.com/opensvc/om3/core/clusterdump" + "github.com/opensvc/om3/core/event" + "github.com/opensvc/om3/core/monitor" + "github.com/opensvc/om3/core/naming" + "github.com/opensvc/om3/core/rawconfig" + "github.com/opensvc/om3/core/streamlog" + "github.com/opensvc/om3/daemon/api" + "github.com/opensvc/om3/daemon/msgbus" + "github.com/opensvc/om3/util/sizeconv" + "github.com/rivo/tview" + "github.com/rs/zerolog" +) + +type ( + viewId int + viewStack []viewId + + App struct { + *monitor.Frame + + eventCount uint64 + + stack []viewId + + app *tview.Application + top *tview.TextView + errs *tview.TextView + textView *tview.TextView + keys *tview.Table + objects *tview.Table + flex *tview.Flex + command *tview.InputField + + client *client.T + + lastDraw time.Time + + viewPath naming.Path + viewNode string + + firstInstanceCol int + firstObjectRow int + + maxRetries int + displayInterval time.Duration + + selectedNodes map[string]any + selectedPaths map[string]any + selectedInstances map[[2]string]any + + errC chan error + restartC chan error + exitFlag atomic.Bool + + logCloser io.Closer + } + + getter interface { + Get() ([]byte, error) + } +) + +const ( + viewObject viewId = iota + viewConfig + viewKey + viewKeys + viewInstance + viewLog +) + +var ( + colorNone = tcell.ColorNone + colorSelected = tcell.ColorDarkSlateGray + colorTitle = tcell.ColorGray + colorHighlight = tcell.ColorWhite + + spin = []rune{'⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'} + spinLen = len(spin) +) + +func main() { + if err := NewApp().Run(); err != nil { + fmt.Fprintln(os.Stderr, err) + } +} + +func (t *App) push(v viewId) { + t.stack = append(t.stack, v) +} + +func (t *App) pop() viewId { + n := len(t.stack) + if n == 0 { + return viewObject + } + v := t.stack[n-1] + t.stack = t.stack[:n-1] + return v +} + +func (t *App) focus() viewId { + n := len(t.stack) + if n == 0 { + return viewObject + } + return t.stack[n-1] +} + +func NewApp() *App { + return &App{ + stack: make([]viewId, 0), + firstInstanceCol: 5, + maxRetries: 600, + displayInterval: 500 * time.Millisecond, + Frame: &monitor.Frame{ + Selector: "*/svc/*", + Sections: []string{}, + }, + selectedNodes: make(map[string]any), + selectedPaths: make(map[string]any), + selectedInstances: make(map[[2]string]any), + errC: make(chan error), + restartC: make(chan error), + } +} + +func (t *App) resetAllSelected() { + t.resetSelectedNodes() + t.resetSelectedPaths() + t.resetSelectedInstances() +} + +func (t *App) initKeysTable() { + table := tview.NewTable() + table.SetBorder(true).SetTitle(fmt.Sprintf("%s keys", t.viewPath)).SetBorderPadding(1, 1, 1, 1) + + onEnter := func(event *tcell.EventKey) { + row, col := table.GetSelection() + if row == 0 { + return + } + key := table.GetCell(row, col).Text + resp, err := t.client.GetObjectKVStoreEntryWithResponse(context.Background(), t.viewPath.Namespace, t.viewPath.Kind, t.viewPath.Name, &api.GetObjectKVStoreEntryParams{ + Key: key, + }) + if err != nil { + t.errorf("%s", err) + return + } + if resp.StatusCode() != http.StatusOK { + t.errorf("status code: %s", resp.Status()) + return + } + + t.initTextView() + text := string(resp.Body) + title := fmt.Sprintf("%s key %s", t.viewPath, key) + t.textView.SetTitle(title) + t.textView.Clear() + fmt.Fprint(t.textView, text) + t.nav(viewKey) + } + + table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyLeft, tcell.KeyRight, tcell.KeyUp, tcell.KeyDown: + table.SetSelectable(true, true) + case tcell.KeyESC: + t.back() + case tcell.KeyEnter: + onEnter(event) + return nil // prevents the default select behaviour + } + switch event.Rune() { + case 'c': + t.onRuneC(event) + case 'h': + t.onRuneH(event) + case 'l': + t.onRuneL(event) + case 's': + t.onRuneS(event) + case 'q': + t.stop() + case ':': + t.onRuneColumn(event) + default: + return event + } + return nil + }) + t.keys = table +} + +func (t *App) initObjectsTable() { + table := tview.NewTable() + + selectedFunc := func(row, column int) { + cell := table.GetCell(row, column) + path := table.GetCell(row, 0).Text + node := table.GetCell(0, column).Text + var selected *bool + switch { + case row < t.firstObjectRow-1: + case row == t.firstObjectRow-1: + v := t.toggleNode(node) + selected = &v + case column == 0: + v := t.togglePath(path) + selected = &v + case column >= t.firstInstanceCol: + v := t.toggleInstance(path, node) + selected = &v + } + if selected != nil && *selected { + cell.SetBackgroundColor(colorSelected) + } else { + cell.SetBackgroundColor(colorNone) + } + } + + table.SetSelectedFunc(selectedFunc) + + setSelection := func(table *tview.Table) { + row, col := table.GetSelection() + cell := table.GetCell(row, col) + cell.SetBackgroundColor(colorSelected) + table.SetCell(row, col, cell) + selectedFunc(row, col) + } + + selectAll := func() { + for i := t.firstObjectRow; i < table.GetRowCount(); i++ { + selectedFunc(i, 0) + } + } + + table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyLeft, tcell.KeyRight, tcell.KeyUp, tcell.KeyDown: + table.SetSelectable(true, true) + case tcell.KeyESC: + t.resetSelectedNodes() + t.resetSelectedPaths() + t.resetSelectedInstances() + case tcell.KeyCtrlA: + selectAll() + case tcell.KeyEnter: + t.onEnter(event) + return nil // prevents the default select behaviour + } + switch event.Rune() { + case ' ': + setSelection(table) + case 'c': + t.onRuneC(event) + case 'h': + t.onRuneH(event) + case 'l': + t.onRuneL(event) + case 's': + t.onRuneS(event) + case 'q': + t.stop() + case ':': + t.onRuneColumn(event) + default: + return event + } + return nil + }) + t.objects = table +} + +func (t *App) initErrsTextView() { + t.errs = tview.NewTextView() + t.errs.SetBorder(false) +} + +func (t *App) initApp() { + t.initObjectsTable() + t.initErrsTextView() + + t.app = tview.NewApplication() + t.flex = tview.NewFlex().SetDirection(tview.FlexRow) + t.flex.AddItem(t.objects, 0, 1, true) + t.app.SetRoot(t.flex, true) +} + +func (t *App) init() error { + if len(os.Args) > 1 { + t.Frame.Selector = os.Args[1] + } + t.initApp() + + if cli, err := client.New(client.WithTimeout(0)); err != nil { + return err + } else { + t.client = cli + } + + monitor.InitColor() + + return nil +} + +func (t *App) Run() error { + if err := t.init(); err != nil { + return err + } + go t.runEventReader() + return t.app.Run() +} + +func (t *App) runEventReader() { + for { + evReader, err := t.client.NewGetEvents().SetSelector(t.Selector).GetReader() + if err != nil { + t.errorf("%s", err) + if t.exitFlag.Load() { + return + } + time.Sleep(10 * time.Millisecond) + continue + } + + statusGetter := t.client.NewGetDaemonStatus().SetSelector(t.Selector) + err = t.do(statusGetter, evReader) + _ = evReader.Close() + if t.exitFlag.Load() { + return + } + if err != nil { + time.Sleep(10 * time.Millisecond) + } + } +} + +func (t *App) do(statusGetter getter, evReader event.ReadCloser) error { + var ( + b []byte + data *clusterdump.Data + err error + + eventC = make(chan event.Event, 100) + dataC = make(chan *clusterdump.Data) + + nextEventID uint64 + + wg = sync.WaitGroup{} + ) + + defer wg.Wait() + + wg.Add(1) + go func() { + defer wg.Done() + defer close(eventC) + + for { + ev, err := evReader.Read() + if err != nil { + err = fmt.Errorf("event queue read error: %w", err) + t.errorf("%s", err) + t.errC <- err + return + } + eventC <- *ev + } + }() + + //t.infof("get daemon status") + b, err = statusGetter.Get() + if err != nil { + return err + } + if err := json.Unmarshal(b, &data); err != nil { + return err + } + + cdata := msgbus.NewClusterData(data) + wg.Add(1) + go func(d *clusterdump.Data) { + defer wg.Done() + t.Current = *d + t.Nodename = data.Daemon.Nodename + t.app.QueueUpdateDraw(func() { + t.updateObjects() + }) + // show data when new data published on dataC + for d := range dataC { + t.Current = *d + t.Nodename = data.Daemon.Nodename + t.eventCount++ + t.app.QueueUpdateDraw(func() { + // TODO: detect if t.updateInstanceView and t.updateConfigView need to be called (config mtime change, ...) + switch t.focus() { + case viewInstance: + t.updateInstanceView() + case viewConfig: + t.updateConfigView() + case viewKeys: + t.updateKeysView() + default: + t.updateObjects() + } + }) + } + }(data.DeepCopy()) + + defer close(dataC) + + ticker := time.NewTicker(t.displayInterval) + defer ticker.Stop() + changes := false + for { + select { + case <-t.restartC: + _ = evReader.Close() + case err := <-t.errC: + return err + case e := <-eventC: + if nextEventID == 0 { + nextEventID = e.ID + } else if e.ID != nextEventID { + err := fmt.Errorf("broken event chain: received event id %d, expected %d", e.ID, nextEventID) + t.errorf("%s", err) + return err + } + nextEventID++ + changes = true + msg, err := msgbus.EventToMessage(e) + if err != nil { + t.errorf("EventToMessage event id %d %s error: %s", e.ID, e.Kind, err) + continue + } + cdata.ApplyMessage(msg) + case <-ticker.C: + if changes { + dataC <- cdata.DeepCopy() + changes = false + } else if t.focus() == viewObject { + t.app.QueueUpdateDraw(func() { + s := fmt.Sprint(time.Now().Truncate(time.Second).Sub(t.lastDraw.Truncate(time.Second))) + t.objects.SetCell(2, 1, tview.NewTableCell(s).SetSelectable(false)) + }) + } + } + } +} + +func (t *App) paths() []string { + paths := make([]string, len(t.Current.Cluster.Object)) + i := 0 + for path := range t.Current.Cluster.Object { + paths[i] = path + i += 1 + } + sort.Strings(paths) + return paths + +} + +func (t *App) updateObjects() { + nodesCells := func(row int, selectable bool) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + t.objects.SetCell(row, t.firstInstanceCol+i, t.cellNode(nodename, selectable)) + } + } + + nodesScoreCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeScore(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + nodesLoadCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeLoad(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + nodesMemCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeMem(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + nodesSwapCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeSwap(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + nodesStateCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeStates(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + t.lastDraw = time.Now() + + t.objects.Clear() + + row := 0 + t.objects.SetCell(row, 0, tview.NewTableCell("CLUSTER").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell(t.Current.Cluster.Config.Name).SetSelectable(true)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("NODE").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesCells(row, false) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("EVENT").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell(fmt.Sprintf("%d", t.eventCount)).SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("SCORE").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesScoreCells(row) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("LAST").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("0s").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell(".LOAD").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesLoadCells(row) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell(".MEM").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesMemCells(row) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell(".SWAP").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesSwapCells(row) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("STATE").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesStateCells(row) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("┼").SetTextColor(colorTitle).SetSelectable(false)) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("PATH").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("AVAIL").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("ORCHESTRATE").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("UP").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesCells(row, true) + + t.firstObjectRow = row + 1 + + t.objects.SetFixed(t.firstObjectRow, 2) + + for _, path := range t.paths() { + row++ + t.objects.SetCell(row, 0, t.cellObjectPath(path)) + t.objects.SetCell(row, 1, t.cellObjectStatus(path)) + t.objects.SetCell(row, 2, t.cellObjectOrchestrate(path)) + t.objects.SetCell(row, 3, t.cellObjectRunning(path)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + for j, nodename := range t.Current.Cluster.Config.Nodes { + t.objects.SetCell(row, 5+j, t.cellInstanceStatus(path, nodename)) + } + } +} + +func (t *App) cellObjectOrchestrate(path string) *tview.TableCell { + s := t.Current.Cluster.Object[path].Orchestrate + return tview.NewTableCell(s).SetSelectable(false) +} + +func (t *App) cellObjectRunning(path string) *tview.TableCell { + s := tview.TranslateANSI(t.StrObjectRunning(path)) + return tview.NewTableCell(s).SetSelectable(false) +} + +func (t *App) cellObjectStatus(path string) *tview.TableCell { + s := tview.TranslateANSI(monitor.StrObjectStatus(t.Current.Cluster.Object[path])) + return tview.NewTableCell(s).SetSelectable(false) +} + +func (t *App) cellInstanceStatus(path, node string) *tview.TableCell { + s := tview.TranslateANSI(t.StrObjectInstance(path, node, t.Current.Cluster.Object[path].Scope)) + cell := tview.NewTableCell(s) + if t.isInstanceSelected(path, node) { + cell.SetBackgroundColor(colorSelected) + } + return cell +} + +func (t *App) cellNode(node string, selectable bool) *tview.TableCell { + cell := tview.NewTableCell(node).SetAttributes(tcell.AttrBold).SetSelectable(selectable) + if selectable && t.isNodeSelected(node) { + cell.SetBackgroundColor(colorSelected) + } + return cell +} + +func (t *App) cellObjectPath(path string) *tview.TableCell { + cell := tview.NewTableCell(path).SetAttributes(tcell.AttrBold) + if t.isPathSelected(path) { + cell.SetBackgroundColor(colorSelected) + } + return cell +} + +func (t *App) toggleInstance(path, node string) bool { + key := [2]string{path, node} + if _, ok := t.selectedInstances[key]; ok { + delete(t.selectedInstances, key) + return false + } else { + t.selectedInstances[key] = nil + t.resetSelectedPaths() + t.resetSelectedNodes() + return true + } +} + +func (t *App) togglePath(key string) bool { + if _, ok := t.selectedPaths[key]; ok { + delete(t.selectedPaths, key) + return false + } else { + t.selectedPaths[key] = nil + t.resetSelectedInstances() + t.resetSelectedNodes() + return true + } +} + +func (t *App) toggleNode(key string) bool { + if _, ok := t.selectedNodes[key]; ok { + delete(t.selectedNodes, key) + return false + } else { + t.selectedNodes[key] = nil + t.resetSelectedInstances() + t.resetSelectedPaths() + return true + } +} + +func (t *App) resetSelectedNodes() { + if len(t.selectedNodes) == 0 { + return + } + t.selectedNodes = make(map[string]any) + for j := t.firstInstanceCol; j < t.objects.GetColumnCount(); j += 1 { + t.objects.GetCell(t.firstObjectRow-1, j).SetBackgroundColor(colorNone) + } +} + +func (t *App) resetSelectedInstances() { + if len(t.selectedInstances) == 0 { + return + } + t.selectedInstances = make(map[[2]string]any) + for i := 1; i < t.objects.GetRowCount(); i += 1 { + for j := t.firstInstanceCol; j < t.objects.GetColumnCount(); j += 1 { + t.objects.GetCell(i, j).SetBackgroundColor(colorNone) + } + } +} + +func (t *App) resetSelectedPaths() { + if len(t.selectedPaths) == 0 { + return + } + t.selectedPaths = make(map[string]any) + for i := 1; i < t.objects.GetRowCount(); i += 1 { + t.objects.GetCell(i, 0).SetBackgroundColor(colorNone) + } +} + +func (t *App) isInstanceSelected(path, node string) bool { + _, ok := t.selectedInstances[[2]string{path, node}] + return ok +} + +func (t *App) isPathSelected(path string) bool { + _, ok := t.selectedPaths[path] + return ok +} + +func (t *App) isNodeSelected(node string) bool { + _, ok := t.selectedNodes[node] + return ok +} + +func (t *App) onRuneColumn(event *tcell.EventKey) { + clean := func() { + t.flex.RemoveItem(t.command) + t.command = nil + t.setFocus() + } + if t.command != nil { + t.back() + clean() + return + } + clusterAction := func(action string) { + switch action { + case "freeze": + t.actionClusterFreeze() + case "unfreeze", "thaw": + t.actionClusterUnfreeze() + } + } + objectAction := func(action string, paths map[string]any) { + switch action { + case "stop": + t.actionStop(paths) + case "start": + t.actionStart(paths) + case "provision": + t.actionProvision(paths) + case "unprovision": + t.actionUnprovision(paths) + case "freeze": + t.actionFreeze(paths) + case "unfreeze", "thaw": + t.actionUnfreeze(paths) + case "switch": + t.actionSwitch(paths) + case "giveback": + t.actionGiveback(paths) + case "abort": + t.actionAbort(paths) + case "purge": + t.actionPurge(paths) + case "delete": + t.actionDelete(paths) + case "restart": + t.actionRestart(paths) + default: + t.errorf("unknown command: %s", action) + } + } + instanceAction := func(action string, keys map[[2]string]any) { + switch action { + case "stop": + t.actionInstanceStop(keys) + case "start": + t.actionInstanceStart(keys) + case "provision": + t.actionInstanceProvision(keys) + case "unprovision": + t.actionInstanceUnprovision(keys) + case "freeze": + t.actionInstanceFreeze(keys) + case "unfreeze", "thaw": + t.actionInstanceUnfreeze(keys) + case "restart": + t.actionInstanceRestart(keys) + case "switch": + t.actionInstanceSwitch(keys) + // case "clear": + // t.actionInstanceClear(keys) + default: + t.errorf("unknown command: %s", action) + } + } + nodeAction := func(action string, nodes map[string]any) { + switch action { + case "daemon restart": + t.actionNodeDaemonRestart(nodes) + case "freeze": + t.actionNodeFreeze(nodes) + case "unfreeze", "thaw": + t.actionNodeUnfreeze(nodes) + case "drain": + t.actionNodeDrain(nodes) + default: + t.errorf("unknown command: %s", action) + } + } + t.command = tview.NewInputField(). + SetLabel(":"). + SetFieldWidth(0). + SetDoneFunc(func(key tcell.Key) { + action := strings.TrimSpace(t.command.GetText()) + switch action { + case "sec": + t.setFilter("*/sec/*") + clean() + return + case "cfg": + t.setFilter("*/cfg/*") + clean() + return + case "usr": + t.setFilter("*/usr/*") + clean() + return + case "svc": + t.setFilter("*/svc/*") + clean() + return + case "vol": + t.setFilter("*/vol/*") + clean() + return + } + switch key { + case tcell.KeyEnter: + switch { + case len(t.selectedPaths) > 0: + objectAction(action, t.selectedPaths) + case len(t.selectedInstances) > 0: + instanceAction(action, t.selectedInstances) + case len(t.selectedNodes) > 0: + nodeAction(action, t.selectedNodes) + default: + row, col := t.objects.GetSelection() + switch { + case row == 0 && col == 1: + clusterAction(action) + case row < t.firstObjectRow-1: + case row == t.firstObjectRow-1: + node := t.objects.GetCell(row, col).Text + selection := make(map[string]any) + selection[node] = nil + nodeAction(action, selection) + case col == 0: + path := t.objects.GetCell(row, 0).Text + selection := make(map[string]any) + selection[path] = nil + objectAction(action, selection) + case col >= t.firstInstanceCol: + path := t.objects.GetCell(row, 0).Text + node := t.objects.GetCell(0, col).Text + selection := make(map[[2]string]any) + selection[[2]string{path, node}] = nil + instanceAction(action, selection) + } + } + clean() + case tcell.KeyEscape: + clean() + } + }) + t.flex.RemoveItem(t.errs) + t.flex.AddItem(t.command, 1, 0, true) + t.app.SetFocus(t.command) +} + +func (t *App) setFilter(s string) { + t.Frame.Selector = s + t.restart() +} + +func (t *App) actionNodeDaemonRestart(nodes map[string]any) { + ctx := context.Background() + for node, _ := range nodes { + _, _ = t.client.PostDaemonRestart(ctx, node) + } +} + +func (t *App) actionNodeDrain(nodes map[string]any) { + ctx := context.Background() + for node, _ := range nodes { + _, _ = t.client.PostPeerActionDrainWithResponse(ctx, node) + } +} + +func (t *App) actionNodeFreeze(nodes map[string]any) { + ctx := context.Background() + for node, _ := range nodes { + _, _ = t.client.PostPeerActionFreezeWithResponse(ctx, node, nil) + } +} + +func (t *App) actionNodeUnfreeze(nodes map[string]any) { + ctx := context.Background() + for node, _ := range nodes { + _, _ = t.client.PostPeerActionUnfreezeWithResponse(ctx, node, nil) + } +} + +func (t *App) actionAbort(paths map[string]any) { + ctx := context.Background() + for path := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionAbortWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionRestart(paths map[string]any) { + ctx := context.Background() + for path := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionRestartWithResponse(ctx, p.Namespace, p.Kind, p.Name, api.PostObjectActionRestart{}) + } +} + +func (t *App) actionInstanceRestart(keys map[[2]string]any) { + ctx := context.Background() + for key := range keys { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostInstanceActionRestartWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) + } +} + +func (t *App) actionInstanceStart(keys map[[2]string]any) { + ctx := context.Background() + for key := range keys { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostInstanceActionStartWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) + } +} + +func (t *App) actionInstanceStop(keys map[[2]string]any) { + ctx := context.Background() + for key, _ := range keys { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostInstanceActionStopWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) + } +} + +func (t *App) actionInstanceProvision(keys map[[2]string]any) { + ctx := context.Background() + for key, _ := range keys { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostInstanceActionProvisionWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) + } +} + +func (t *App) actionInstanceUnprovision(keys map[[2]string]any) { + ctx := context.Background() + for key, _ := range keys { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostInstanceActionUnprovisionWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) + } +} + +func (t *App) actionInstanceFreeze(keys map[[2]string]any) { + ctx := context.Background() + for key, _ := range keys { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostInstanceActionFreezeWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) + } +} + +func (t *App) actionInstanceUnfreeze(keys map[[2]string]any) { + ctx := context.Background() + for key, _ := range keys { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostInstanceActionUnfreezeWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) + } +} + +func (t *App) actionInstanceSwitch(keys map[[2]string]any) { + ctx := context.Background() + m := make(map[string][]string) + for key, _ := range keys { + path := key[0] + node := key[1] + if l, ok := m[path]; ok { + l = append(l, node) + m[path] = l + } else { + l = append([]string{}, node) + m[path] = l + } + } + for path, nodes := range m { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + body := api.PostObjectActionSwitch{ + Destination: nodes, + } + _, _ = t.client.PostObjectActionSwitchWithResponse(ctx, p.Namespace, p.Kind, p.Name, body) + } +} + +func (t *App) actionStop(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionStopWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionPurge(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionPurgeWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionDelete(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionDeleteWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionStart(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionStartWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionProvision(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionProvisionWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionUnprovision(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionUnprovisionWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionFreeze(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionFreezeWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionUnfreeze(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionUnfreezeWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionSwitch(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + body := api.PostObjectActionSwitch{} + _, _ = t.client.PostObjectActionSwitchWithResponse(ctx, p.Namespace, p.Kind, p.Name, body) + } +} + +func (t *App) actionGiveback(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionGivebackWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionClusterFreeze() { + ctx := context.Background() + _, _ = t.client.PostClusterActionFreezeWithResponse(ctx) +} + +func (t *App) actionClusterUnfreeze() { + ctx := context.Background() + _, _ = t.client.PostClusterActionUnfreezeWithResponse(ctx) +} + +func (t *App) onRuneH(event *tcell.EventKey) { + help := ` + Command mode Shortcuts + + : Enter command mode + ESC Exit command mode + Enter Apply command to the selected cells + + Selection Shortcuts + + Up,Right,Down,Left Move cursor + SPACE Select the cell + ESC Reset selection + Ctrl-a Invert object selection + + Misc Shortcuts + + c Show object configuration + h Show this help + l Show node, object or instance logs + q Quit + Enter Show instance status + ESC Close popup + + Cluster commands: + + freeze, unfreeze + + Object commands: + + abort, delete, freeze, giveback, provision, purge, start, stop, switch, + unfreeze, unprovision + + Instance commands: + + delete, freeze, provision, start, stop, switch, unfreeze, unprovision + + Node commands: + + drain freeze, unfreeze + +` + t.initTextView() + t.textView.SetTitle("Help") + fmt.Fprint(t.textView, help) +} + +func (t *App) onRuneS(event *tcell.EventKey) { +} + +func (t *App) onRuneL(event *tcell.EventKey) { + row, col := t.objects.GetSelection() + t.viewNode = "" + t.viewPath = naming.Path{} + if row >= t.firstObjectRow { + path := t.objects.GetCell(row, 0).Text + p, err := naming.ParsePath(path) + if err != nil { + return + } + t.viewPath = p + } + if col >= t.firstInstanceCol { + t.viewNode = t.objects.GetCell(t.firstObjectRow-1, col).Text + } + + title := func() string { + switch { + case !t.viewPath.IsZero() && t.viewNode != "": + return fmt.Sprintf("%s@%s log", t.viewPath, t.viewNode) + case !t.viewPath.IsZero(): + return fmt.Sprintf("%s log", t.viewPath) + case t.viewNode != "": + return fmt.Sprintf("%s log", t.viewNode) + default: + return "" + } + } + + t.initTextView() + t.nav(viewLog) + t.textView.SetDynamicColors(true) + t.textView.SetTitle(title()) + t.textView.SetChangedFunc(func() { + t.textView.ScrollToEnd() + }) + + t.textView.Clear() + + lines := 50 + follow := true + log := t.client.NewGetLogs(t.viewNode). + //SetFilters(nil). + SetLines(&lines). + SetFollow(&follow) + if !t.viewPath.IsZero() { + l := naming.Paths{t.viewPath}.StrSlice() + log.SetPaths(&l) + } + reader, err := log.GetReader() + if err != nil { + t.errorf("%s", err) + return + } + t.logCloser = reader + + w := zerolog.NewConsoleWriter() + w.Out = tview.ANSIWriter(t.textView) + w.TimeFormat = "2006-01-02T15:04:05.000Z07:00" + w.FormatFieldName = func(i any) string { return "" } + w.FormatFieldValue = func(i any) string { return "" } + w.FormatMessage = func(i any) string { + return rawconfig.Colorize.Bold(i) + } + + go func() { + for { + event, err := reader.Read() + if errors.Is(err, io.EOF) { + break + } else if errors.Is(err, context.Canceled) { + break + } else if err != nil { + t.errorf("%s", err) + break + } + rec, err := streamlog.NewEvent(event.Data) + if err != nil { + t.errorf("%s", err) + break + } + switch s := rec.M["JSON"].(type) { + case string: + _, _ = w.Write([]byte(s)) + } + } + }() +} + +func (t *App) onEnter(event *tcell.EventKey) { + row, col := t.objects.GetSelection() + t.viewPath = naming.Path{} + t.viewNode = "" + if row >= t.firstObjectRow { + path := t.objects.GetCell(row, 0).Text + p, err := naming.ParsePath(path) + if err != nil { + return + } + t.viewPath = p + } + if col >= t.firstInstanceCol { + node := t.objects.GetCell(t.firstObjectRow-1, col).Text + t.viewNode = node + } + switch { + case !t.viewPath.IsZero() && t.viewNode != "": + t.initTextView() + t.nav(viewInstance) + case t.viewPath.Kind == naming.KindCfg || t.viewPath.Kind == naming.KindSec: + t.nav(viewKeys) + } +} + +func (t *App) setFocus() { + switch t.focus() { + case viewConfig: + t.app.SetFocus(t.textView) + case viewInstance: + t.app.SetFocus(t.textView) + case viewLog: + t.app.SetFocus(t.textView) + case viewKeys: + t.app.SetFocus(t.keys) + default: + t.app.SetFocus(t.objects) + } +} + +func (t *App) updateKeysView() { + if t.viewPath.IsZero() { + return + } + t.initKeysTable() + resp, err := t.client.GetObjectKVStoreKeysWithResponse(context.Background(), t.viewPath.Namespace, t.viewPath.Kind, t.viewPath.Name) + if err != nil { + return + } + if resp.StatusCode() != http.StatusOK { + return + } + t.keys.Clear() + t.keys.SetCell(0, 0, tview.NewTableCell("NAME").SetTextColor(colorTitle)) + t.keys.SetCell(0, 1, tview.NewTableCell("SIZE").SetTextColor(colorTitle)) + for i, key := range resp.JSON200.Items { + row := 1 + i + t.keys.SetCell(row, 0, tview.NewTableCell(key.Key).SetSelectable(true)) + t.keys.SetCell(row, 1, tview.NewTableCell(sizeconv.BSizeCompact(float64(key.Size)))) + } +} + +func (t *App) updateInstanceView() { + digest := t.Frame.Current.GetObjectStatus(t.viewPath) + text := tview.TranslateANSI(digest.Render([]string{t.viewNode})) + t.initTextView() + title := fmt.Sprintf("%s@%s status", t.viewPath, t.viewNode) + t.textView.SetDynamicColors(true) + t.textView.SetTitle(title) + t.textView.Clear() + fmt.Fprint(t.textView, text) +} + +func (t *App) onRuneC(event *tcell.EventKey) { + row, _ := t.objects.GetSelection() + path := t.objects.GetCell(row, 0).Text + p, err := naming.ParsePath(path) + if err != nil { + return + } + t.viewPath = p + t.viewNode = "" + t.initTextView() + t.updateConfigView() + t.nav(viewConfig) +} + +func (t *App) updateConfigView() { + if t.viewPath.IsZero() { + return + } + resp, err := t.client.GetObjectConfigFileWithResponse(context.Background(), t.viewPath.Namespace, t.viewPath.Kind, t.viewPath.Name) + if err != nil { + return + } + if resp.StatusCode() != http.StatusOK { + return + } + + text := tview.TranslateANSI(string(resp.Body)) + title := fmt.Sprintf("%s configuration", t.viewPath) + t.textView.SetDynamicColors(false) + t.textView.SetTitle(title) + t.textView.Clear() + fmt.Fprint(t.textView, text) +} + +func (t *App) infof(format string, args ...any) { + t.printf(tcell.ColorGray, format, args...) +} + +func (t *App) warnf(format string, args ...any) { + t.printf(tcell.ColorOrange, format, args...) +} + +func (t *App) errorf(format string, args ...any) { + t.printf(tcell.ColorRed, format, args...) +} + +func (t *App) printf(color tcell.Color, format string, args ...any) { + t.flex.AddItem(t.errs, 1, 0, false) + t.errs.Clear() + t.errs.SetBackgroundColor(color) + fmt.Fprintf(t.errs, format, args...) + time.AfterFunc(5*time.Second, func() { + t.flex.RemoveItem(t.errs) + }) +} + +func (t *App) nav(to viewId) { + from := t.focus() + t.push(to) + if to == from { + return + } + t.navFromTo(from, to) +} + +func (t *App) back() { + from := t.pop() + to := t.focus() + if to == from { + return + } + t.navFromTo(from, to) +} + +func (t *App) navFromTo(from, to viewId) { + t.flex.Clear() + switch from { + case viewObject: + case viewLog: + t.textView.SetChangedFunc(nil) + t.textView = nil + t.logCloser.Close() + case viewConfig, viewInstance, viewKey: + t.textView = nil + case viewKeys: + t.keys = nil + } + switch to { + case viewLog, viewConfig, viewKey: + t.flex.AddItem(t.textView, 0, 1, true) + t.app.SetFocus(t.textView) + case viewInstance: + t.flex.AddItem(t.textView, 0, 1, true) + t.app.SetFocus(t.textView) + t.updateInstanceView() + case viewKeys: + t.updateKeysView() + t.flex.AddItem(t.keys, 0, 1, true) + t.app.SetFocus(t.keys) + case viewObject: + t.flex.AddItem(t.objects, 0, 1, true) + t.app.SetFocus(t.objects) + t.updateObjects() + } + t.flex.AddItem(t.errs, 1, 0, false) +} + +func (t *App) initTextView() { + if t.textView != nil { + return + } + v := tview.NewTextView() + v.SetScrollable(true) + v.SetBorder(true) + v.SetBorderPadding(1, 1, 1, 1) + v.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyESC: + t.back() + } + switch event.Rune() { + case 'q': + t.stop() + case ':': + t.onRuneColumn(event) + default: + return event + } + return nil + }) + + t.textView = v + return +} + +func (t *App) restart() { + t.restartC <- nil +} + +func (t *App) stop() { + t.exitFlag.Store(true) + t.errC <- nil + t.app.Stop() +} diff --git a/go.mod b/go.mod index 7873bb377..2cfab1742 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/fatih/color v1.16.0 github.com/fsnotify/fsnotify v1.6.0 github.com/g8rswimmer/error-chain v1.0.0 - github.com/gdamore/tcell/v2 v2.3.1 + github.com/gdamore/tcell/v2 v2.7.1 github.com/getkin/kin-openapi v0.122.0 github.com/go-chi/jwtauth/v5 v5.0.2 github.com/go-ping/ping v0.0.0-20210506233800-ff8be3320020 @@ -141,7 +141,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/tview v0.0.0-20240921122403-a64fc48d7654 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/safchain/ethtool v0.0.0-20200218184317-f459e2d13664 // indirect github.com/sirupsen/logrus v1.9.0 // indirect diff --git a/go.sum b/go.sum index 0328c0071..6c25f362c 100644 --- a/go.sum +++ b/go.sum @@ -140,6 +140,8 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.3.1 h1:htEXmKMzurgcnNH3VqQA7GYlCpdl9LCSNpzFpZOKhJE= github.com/gdamore/tcell/v2 v2.3.1/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= +github.com/gdamore/tcell/v2 v2.7.1 h1:TiCcmpWHiAU7F0rA2I3S2Y4mmLmO9KHxJ7E1QhYzQbc= +github.com/gdamore/tcell/v2 v2.7.1/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= github.com/getkin/kin-openapi v0.122.0 h1:WB9Jbl0Hp/T79/JF9xlSW5Kl9uYdk/AWD0yAd9HOM10= github.com/getkin/kin-openapi v0.122.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -502,9 +504,14 @@ github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/retailnext/cannula v0.0.0-20160516234737-f1c21e7f5695 h1:06boav/LEzK6QKUPmQsauFd5x7dt9Ljn8sb+tVjwRXw= github.com/retailnext/cannula v0.0.0-20160516234737-f1c21e7f5695/go.mod h1:1bRkDo7/haUZVrwXXTbkXYjXA0Su45oI519pTfZM1WU= +github.com/rivo/tview v0.0.0-20240921122403-a64fc48d7654 h1:oa+fljZiaJUVyiT7WgIM3OhirtwBm0LJA97LvWUlBu8= +github.com/rivo/tview v0.0.0-20240921122403-a64fc48d7654/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -590,6 +597,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zcalusic/sysinfo v0.0.0-20210831153053-2c6e1d254246 h1:IPCi0C6XVSrBw6N6awpC+zl29kSJ7z5X+SFvw89wOcQ= github.com/zcalusic/sysinfo v0.0.0-20210831153053-2c6e1d254246/go.mod h1:WGLNaWsjKQ2gXmAHh+MQztgu3FLFAnOFJjFzhpgShCY= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -609,6 +617,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= @@ -638,6 +647,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -680,6 +691,8 @@ golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -696,6 +709,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -748,15 +763,22 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -766,6 +788,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -801,6 +824,8 @@ golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 35ebab1b1d272c01d4e85c57bf6bbd2e09024cb1 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Wed, 23 Oct 2024 18:31:58 +0200 Subject: [PATCH 09/17] Move ot to ox * add key and object config edit * add header * add nav stack * don't use borders --- Makefile | 6 +- cmd/ot/main.go | 1538 ------------------------------ core/ox/root.go | 4 + core/oxcmd/lib_remote_config.go | 25 - core/oxcmd/object_edit_config.go | 4 +- core/oxcmd/object_edit_key.go | 4 +- 6 files changed, 9 insertions(+), 1572 deletions(-) delete mode 100644 cmd/ot/main.go diff --git a/Makefile b/Makefile index dbdec0e6f..4a30c9dd1 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,6 @@ PREFIX ?= /usr DIST := dist OM := bin/om -OT := bin/ot OX := bin/ox COMPOBJ := bin/compobj COMPOBJ_D := share/opensvc/compliance @@ -26,7 +25,7 @@ VERSION := $(shell git describe --tags --abbrev) all: clean vet test race build dist -build: version api om ot ox compobj +build: version api om ox compobj deps: $(GOINSTALL) github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen@latest @@ -42,9 +41,6 @@ clean: om: $(GOBUILD) -o $(OM) ./cmd/om/ -ot: - $(GOBUILD) -o $(OT) ./cmd/ot/ - ox: $(GOBUILD) -o $(OX) ./cmd/ox/ diff --git a/cmd/ot/main.go b/cmd/ot/main.go deleted file mode 100644 index 16cdc6b56..000000000 --- a/cmd/ot/main.go +++ /dev/null @@ -1,1538 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "os" - "sort" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/gdamore/tcell/v2" - "github.com/opensvc/om3/core/client" - "github.com/opensvc/om3/core/clusterdump" - "github.com/opensvc/om3/core/event" - "github.com/opensvc/om3/core/monitor" - "github.com/opensvc/om3/core/naming" - "github.com/opensvc/om3/core/rawconfig" - "github.com/opensvc/om3/core/streamlog" - "github.com/opensvc/om3/daemon/api" - "github.com/opensvc/om3/daemon/msgbus" - "github.com/opensvc/om3/util/sizeconv" - "github.com/rivo/tview" - "github.com/rs/zerolog" -) - -type ( - viewId int - viewStack []viewId - - App struct { - *monitor.Frame - - eventCount uint64 - - stack []viewId - - app *tview.Application - top *tview.TextView - errs *tview.TextView - textView *tview.TextView - keys *tview.Table - objects *tview.Table - flex *tview.Flex - command *tview.InputField - - client *client.T - - lastDraw time.Time - - viewPath naming.Path - viewNode string - - firstInstanceCol int - firstObjectRow int - - maxRetries int - displayInterval time.Duration - - selectedNodes map[string]any - selectedPaths map[string]any - selectedInstances map[[2]string]any - - errC chan error - restartC chan error - exitFlag atomic.Bool - - logCloser io.Closer - } - - getter interface { - Get() ([]byte, error) - } -) - -const ( - viewObject viewId = iota - viewConfig - viewKey - viewKeys - viewInstance - viewLog -) - -var ( - colorNone = tcell.ColorNone - colorSelected = tcell.ColorDarkSlateGray - colorTitle = tcell.ColorGray - colorHighlight = tcell.ColorWhite - - spin = []rune{'⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'} - spinLen = len(spin) -) - -func main() { - if err := NewApp().Run(); err != nil { - fmt.Fprintln(os.Stderr, err) - } -} - -func (t *App) push(v viewId) { - t.stack = append(t.stack, v) -} - -func (t *App) pop() viewId { - n := len(t.stack) - if n == 0 { - return viewObject - } - v := t.stack[n-1] - t.stack = t.stack[:n-1] - return v -} - -func (t *App) focus() viewId { - n := len(t.stack) - if n == 0 { - return viewObject - } - return t.stack[n-1] -} - -func NewApp() *App { - return &App{ - stack: make([]viewId, 0), - firstInstanceCol: 5, - maxRetries: 600, - displayInterval: 500 * time.Millisecond, - Frame: &monitor.Frame{ - Selector: "*/svc/*", - Sections: []string{}, - }, - selectedNodes: make(map[string]any), - selectedPaths: make(map[string]any), - selectedInstances: make(map[[2]string]any), - errC: make(chan error), - restartC: make(chan error), - } -} - -func (t *App) resetAllSelected() { - t.resetSelectedNodes() - t.resetSelectedPaths() - t.resetSelectedInstances() -} - -func (t *App) initKeysTable() { - table := tview.NewTable() - table.SetBorder(true).SetTitle(fmt.Sprintf("%s keys", t.viewPath)).SetBorderPadding(1, 1, 1, 1) - - onEnter := func(event *tcell.EventKey) { - row, col := table.GetSelection() - if row == 0 { - return - } - key := table.GetCell(row, col).Text - resp, err := t.client.GetObjectKVStoreEntryWithResponse(context.Background(), t.viewPath.Namespace, t.viewPath.Kind, t.viewPath.Name, &api.GetObjectKVStoreEntryParams{ - Key: key, - }) - if err != nil { - t.errorf("%s", err) - return - } - if resp.StatusCode() != http.StatusOK { - t.errorf("status code: %s", resp.Status()) - return - } - - t.initTextView() - text := string(resp.Body) - title := fmt.Sprintf("%s key %s", t.viewPath, key) - t.textView.SetTitle(title) - t.textView.Clear() - fmt.Fprint(t.textView, text) - t.nav(viewKey) - } - - table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { - case tcell.KeyLeft, tcell.KeyRight, tcell.KeyUp, tcell.KeyDown: - table.SetSelectable(true, true) - case tcell.KeyESC: - t.back() - case tcell.KeyEnter: - onEnter(event) - return nil // prevents the default select behaviour - } - switch event.Rune() { - case 'c': - t.onRuneC(event) - case 'h': - t.onRuneH(event) - case 'l': - t.onRuneL(event) - case 's': - t.onRuneS(event) - case 'q': - t.stop() - case ':': - t.onRuneColumn(event) - default: - return event - } - return nil - }) - t.keys = table -} - -func (t *App) initObjectsTable() { - table := tview.NewTable() - - selectedFunc := func(row, column int) { - cell := table.GetCell(row, column) - path := table.GetCell(row, 0).Text - node := table.GetCell(0, column).Text - var selected *bool - switch { - case row < t.firstObjectRow-1: - case row == t.firstObjectRow-1: - v := t.toggleNode(node) - selected = &v - case column == 0: - v := t.togglePath(path) - selected = &v - case column >= t.firstInstanceCol: - v := t.toggleInstance(path, node) - selected = &v - } - if selected != nil && *selected { - cell.SetBackgroundColor(colorSelected) - } else { - cell.SetBackgroundColor(colorNone) - } - } - - table.SetSelectedFunc(selectedFunc) - - setSelection := func(table *tview.Table) { - row, col := table.GetSelection() - cell := table.GetCell(row, col) - cell.SetBackgroundColor(colorSelected) - table.SetCell(row, col, cell) - selectedFunc(row, col) - } - - selectAll := func() { - for i := t.firstObjectRow; i < table.GetRowCount(); i++ { - selectedFunc(i, 0) - } - } - - table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { - case tcell.KeyLeft, tcell.KeyRight, tcell.KeyUp, tcell.KeyDown: - table.SetSelectable(true, true) - case tcell.KeyESC: - t.resetSelectedNodes() - t.resetSelectedPaths() - t.resetSelectedInstances() - case tcell.KeyCtrlA: - selectAll() - case tcell.KeyEnter: - t.onEnter(event) - return nil // prevents the default select behaviour - } - switch event.Rune() { - case ' ': - setSelection(table) - case 'c': - t.onRuneC(event) - case 'h': - t.onRuneH(event) - case 'l': - t.onRuneL(event) - case 's': - t.onRuneS(event) - case 'q': - t.stop() - case ':': - t.onRuneColumn(event) - default: - return event - } - return nil - }) - t.objects = table -} - -func (t *App) initErrsTextView() { - t.errs = tview.NewTextView() - t.errs.SetBorder(false) -} - -func (t *App) initApp() { - t.initObjectsTable() - t.initErrsTextView() - - t.app = tview.NewApplication() - t.flex = tview.NewFlex().SetDirection(tview.FlexRow) - t.flex.AddItem(t.objects, 0, 1, true) - t.app.SetRoot(t.flex, true) -} - -func (t *App) init() error { - if len(os.Args) > 1 { - t.Frame.Selector = os.Args[1] - } - t.initApp() - - if cli, err := client.New(client.WithTimeout(0)); err != nil { - return err - } else { - t.client = cli - } - - monitor.InitColor() - - return nil -} - -func (t *App) Run() error { - if err := t.init(); err != nil { - return err - } - go t.runEventReader() - return t.app.Run() -} - -func (t *App) runEventReader() { - for { - evReader, err := t.client.NewGetEvents().SetSelector(t.Selector).GetReader() - if err != nil { - t.errorf("%s", err) - if t.exitFlag.Load() { - return - } - time.Sleep(10 * time.Millisecond) - continue - } - - statusGetter := t.client.NewGetDaemonStatus().SetSelector(t.Selector) - err = t.do(statusGetter, evReader) - _ = evReader.Close() - if t.exitFlag.Load() { - return - } - if err != nil { - time.Sleep(10 * time.Millisecond) - } - } -} - -func (t *App) do(statusGetter getter, evReader event.ReadCloser) error { - var ( - b []byte - data *clusterdump.Data - err error - - eventC = make(chan event.Event, 100) - dataC = make(chan *clusterdump.Data) - - nextEventID uint64 - - wg = sync.WaitGroup{} - ) - - defer wg.Wait() - - wg.Add(1) - go func() { - defer wg.Done() - defer close(eventC) - - for { - ev, err := evReader.Read() - if err != nil { - err = fmt.Errorf("event queue read error: %w", err) - t.errorf("%s", err) - t.errC <- err - return - } - eventC <- *ev - } - }() - - //t.infof("get daemon status") - b, err = statusGetter.Get() - if err != nil { - return err - } - if err := json.Unmarshal(b, &data); err != nil { - return err - } - - cdata := msgbus.NewClusterData(data) - wg.Add(1) - go func(d *clusterdump.Data) { - defer wg.Done() - t.Current = *d - t.Nodename = data.Daemon.Nodename - t.app.QueueUpdateDraw(func() { - t.updateObjects() - }) - // show data when new data published on dataC - for d := range dataC { - t.Current = *d - t.Nodename = data.Daemon.Nodename - t.eventCount++ - t.app.QueueUpdateDraw(func() { - // TODO: detect if t.updateInstanceView and t.updateConfigView need to be called (config mtime change, ...) - switch t.focus() { - case viewInstance: - t.updateInstanceView() - case viewConfig: - t.updateConfigView() - case viewKeys: - t.updateKeysView() - default: - t.updateObjects() - } - }) - } - }(data.DeepCopy()) - - defer close(dataC) - - ticker := time.NewTicker(t.displayInterval) - defer ticker.Stop() - changes := false - for { - select { - case <-t.restartC: - _ = evReader.Close() - case err := <-t.errC: - return err - case e := <-eventC: - if nextEventID == 0 { - nextEventID = e.ID - } else if e.ID != nextEventID { - err := fmt.Errorf("broken event chain: received event id %d, expected %d", e.ID, nextEventID) - t.errorf("%s", err) - return err - } - nextEventID++ - changes = true - msg, err := msgbus.EventToMessage(e) - if err != nil { - t.errorf("EventToMessage event id %d %s error: %s", e.ID, e.Kind, err) - continue - } - cdata.ApplyMessage(msg) - case <-ticker.C: - if changes { - dataC <- cdata.DeepCopy() - changes = false - } else if t.focus() == viewObject { - t.app.QueueUpdateDraw(func() { - s := fmt.Sprint(time.Now().Truncate(time.Second).Sub(t.lastDraw.Truncate(time.Second))) - t.objects.SetCell(2, 1, tview.NewTableCell(s).SetSelectable(false)) - }) - } - } - } -} - -func (t *App) paths() []string { - paths := make([]string, len(t.Current.Cluster.Object)) - i := 0 - for path := range t.Current.Cluster.Object { - paths[i] = path - i += 1 - } - sort.Strings(paths) - return paths - -} - -func (t *App) updateObjects() { - nodesCells := func(row int, selectable bool) { - for i, nodename := range t.Current.Cluster.Config.Nodes { - t.objects.SetCell(row, t.firstInstanceCol+i, t.cellNode(nodename, selectable)) - } - } - - nodesScoreCells := func(row int) { - for i, nodename := range t.Current.Cluster.Config.Nodes { - s := tview.TranslateANSI(t.StrNodeScore(nodename)) - t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) - } - } - - nodesLoadCells := func(row int) { - for i, nodename := range t.Current.Cluster.Config.Nodes { - s := tview.TranslateANSI(t.StrNodeLoad(nodename)) - t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) - } - } - - nodesMemCells := func(row int) { - for i, nodename := range t.Current.Cluster.Config.Nodes { - s := tview.TranslateANSI(t.StrNodeMem(nodename)) - t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) - } - } - - nodesSwapCells := func(row int) { - for i, nodename := range t.Current.Cluster.Config.Nodes { - s := tview.TranslateANSI(t.StrNodeSwap(nodename)) - t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) - } - } - - nodesStateCells := func(row int) { - for i, nodename := range t.Current.Cluster.Config.Nodes { - s := tview.TranslateANSI(t.StrNodeStates(nodename)) - t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) - } - } - - t.lastDraw = time.Now() - - t.objects.Clear() - - row := 0 - t.objects.SetCell(row, 0, tview.NewTableCell("CLUSTER").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell(t.Current.Cluster.Config.Name).SetSelectable(true)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell("NODE").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesCells(row, false) - - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("EVENT").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell(fmt.Sprintf("%d", t.eventCount)).SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell("SCORE").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesScoreCells(row) - - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("LAST").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("0s").SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell(".LOAD").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesLoadCells(row) - - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell(".MEM").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesMemCells(row) - - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell(".SWAP").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesSwapCells(row) - - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell("STATE").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesStateCells(row) - - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("┼").SetTextColor(colorTitle).SetSelectable(false)) - - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("PATH").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("AVAIL").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("ORCHESTRATE").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell("UP").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesCells(row, true) - - t.firstObjectRow = row + 1 - - t.objects.SetFixed(t.firstObjectRow, 2) - - for _, path := range t.paths() { - row++ - t.objects.SetCell(row, 0, t.cellObjectPath(path)) - t.objects.SetCell(row, 1, t.cellObjectStatus(path)) - t.objects.SetCell(row, 2, t.cellObjectOrchestrate(path)) - t.objects.SetCell(row, 3, t.cellObjectRunning(path)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - for j, nodename := range t.Current.Cluster.Config.Nodes { - t.objects.SetCell(row, 5+j, t.cellInstanceStatus(path, nodename)) - } - } -} - -func (t *App) cellObjectOrchestrate(path string) *tview.TableCell { - s := t.Current.Cluster.Object[path].Orchestrate - return tview.NewTableCell(s).SetSelectable(false) -} - -func (t *App) cellObjectRunning(path string) *tview.TableCell { - s := tview.TranslateANSI(t.StrObjectRunning(path)) - return tview.NewTableCell(s).SetSelectable(false) -} - -func (t *App) cellObjectStatus(path string) *tview.TableCell { - s := tview.TranslateANSI(monitor.StrObjectStatus(t.Current.Cluster.Object[path])) - return tview.NewTableCell(s).SetSelectable(false) -} - -func (t *App) cellInstanceStatus(path, node string) *tview.TableCell { - s := tview.TranslateANSI(t.StrObjectInstance(path, node, t.Current.Cluster.Object[path].Scope)) - cell := tview.NewTableCell(s) - if t.isInstanceSelected(path, node) { - cell.SetBackgroundColor(colorSelected) - } - return cell -} - -func (t *App) cellNode(node string, selectable bool) *tview.TableCell { - cell := tview.NewTableCell(node).SetAttributes(tcell.AttrBold).SetSelectable(selectable) - if selectable && t.isNodeSelected(node) { - cell.SetBackgroundColor(colorSelected) - } - return cell -} - -func (t *App) cellObjectPath(path string) *tview.TableCell { - cell := tview.NewTableCell(path).SetAttributes(tcell.AttrBold) - if t.isPathSelected(path) { - cell.SetBackgroundColor(colorSelected) - } - return cell -} - -func (t *App) toggleInstance(path, node string) bool { - key := [2]string{path, node} - if _, ok := t.selectedInstances[key]; ok { - delete(t.selectedInstances, key) - return false - } else { - t.selectedInstances[key] = nil - t.resetSelectedPaths() - t.resetSelectedNodes() - return true - } -} - -func (t *App) togglePath(key string) bool { - if _, ok := t.selectedPaths[key]; ok { - delete(t.selectedPaths, key) - return false - } else { - t.selectedPaths[key] = nil - t.resetSelectedInstances() - t.resetSelectedNodes() - return true - } -} - -func (t *App) toggleNode(key string) bool { - if _, ok := t.selectedNodes[key]; ok { - delete(t.selectedNodes, key) - return false - } else { - t.selectedNodes[key] = nil - t.resetSelectedInstances() - t.resetSelectedPaths() - return true - } -} - -func (t *App) resetSelectedNodes() { - if len(t.selectedNodes) == 0 { - return - } - t.selectedNodes = make(map[string]any) - for j := t.firstInstanceCol; j < t.objects.GetColumnCount(); j += 1 { - t.objects.GetCell(t.firstObjectRow-1, j).SetBackgroundColor(colorNone) - } -} - -func (t *App) resetSelectedInstances() { - if len(t.selectedInstances) == 0 { - return - } - t.selectedInstances = make(map[[2]string]any) - for i := 1; i < t.objects.GetRowCount(); i += 1 { - for j := t.firstInstanceCol; j < t.objects.GetColumnCount(); j += 1 { - t.objects.GetCell(i, j).SetBackgroundColor(colorNone) - } - } -} - -func (t *App) resetSelectedPaths() { - if len(t.selectedPaths) == 0 { - return - } - t.selectedPaths = make(map[string]any) - for i := 1; i < t.objects.GetRowCount(); i += 1 { - t.objects.GetCell(i, 0).SetBackgroundColor(colorNone) - } -} - -func (t *App) isInstanceSelected(path, node string) bool { - _, ok := t.selectedInstances[[2]string{path, node}] - return ok -} - -func (t *App) isPathSelected(path string) bool { - _, ok := t.selectedPaths[path] - return ok -} - -func (t *App) isNodeSelected(node string) bool { - _, ok := t.selectedNodes[node] - return ok -} - -func (t *App) onRuneColumn(event *tcell.EventKey) { - clean := func() { - t.flex.RemoveItem(t.command) - t.command = nil - t.setFocus() - } - if t.command != nil { - t.back() - clean() - return - } - clusterAction := func(action string) { - switch action { - case "freeze": - t.actionClusterFreeze() - case "unfreeze", "thaw": - t.actionClusterUnfreeze() - } - } - objectAction := func(action string, paths map[string]any) { - switch action { - case "stop": - t.actionStop(paths) - case "start": - t.actionStart(paths) - case "provision": - t.actionProvision(paths) - case "unprovision": - t.actionUnprovision(paths) - case "freeze": - t.actionFreeze(paths) - case "unfreeze", "thaw": - t.actionUnfreeze(paths) - case "switch": - t.actionSwitch(paths) - case "giveback": - t.actionGiveback(paths) - case "abort": - t.actionAbort(paths) - case "purge": - t.actionPurge(paths) - case "delete": - t.actionDelete(paths) - case "restart": - t.actionRestart(paths) - default: - t.errorf("unknown command: %s", action) - } - } - instanceAction := func(action string, keys map[[2]string]any) { - switch action { - case "stop": - t.actionInstanceStop(keys) - case "start": - t.actionInstanceStart(keys) - case "provision": - t.actionInstanceProvision(keys) - case "unprovision": - t.actionInstanceUnprovision(keys) - case "freeze": - t.actionInstanceFreeze(keys) - case "unfreeze", "thaw": - t.actionInstanceUnfreeze(keys) - case "restart": - t.actionInstanceRestart(keys) - case "switch": - t.actionInstanceSwitch(keys) - // case "clear": - // t.actionInstanceClear(keys) - default: - t.errorf("unknown command: %s", action) - } - } - nodeAction := func(action string, nodes map[string]any) { - switch action { - case "daemon restart": - t.actionNodeDaemonRestart(nodes) - case "freeze": - t.actionNodeFreeze(nodes) - case "unfreeze", "thaw": - t.actionNodeUnfreeze(nodes) - case "drain": - t.actionNodeDrain(nodes) - default: - t.errorf("unknown command: %s", action) - } - } - t.command = tview.NewInputField(). - SetLabel(":"). - SetFieldWidth(0). - SetDoneFunc(func(key tcell.Key) { - action := strings.TrimSpace(t.command.GetText()) - switch action { - case "sec": - t.setFilter("*/sec/*") - clean() - return - case "cfg": - t.setFilter("*/cfg/*") - clean() - return - case "usr": - t.setFilter("*/usr/*") - clean() - return - case "svc": - t.setFilter("*/svc/*") - clean() - return - case "vol": - t.setFilter("*/vol/*") - clean() - return - } - switch key { - case tcell.KeyEnter: - switch { - case len(t.selectedPaths) > 0: - objectAction(action, t.selectedPaths) - case len(t.selectedInstances) > 0: - instanceAction(action, t.selectedInstances) - case len(t.selectedNodes) > 0: - nodeAction(action, t.selectedNodes) - default: - row, col := t.objects.GetSelection() - switch { - case row == 0 && col == 1: - clusterAction(action) - case row < t.firstObjectRow-1: - case row == t.firstObjectRow-1: - node := t.objects.GetCell(row, col).Text - selection := make(map[string]any) - selection[node] = nil - nodeAction(action, selection) - case col == 0: - path := t.objects.GetCell(row, 0).Text - selection := make(map[string]any) - selection[path] = nil - objectAction(action, selection) - case col >= t.firstInstanceCol: - path := t.objects.GetCell(row, 0).Text - node := t.objects.GetCell(0, col).Text - selection := make(map[[2]string]any) - selection[[2]string{path, node}] = nil - instanceAction(action, selection) - } - } - clean() - case tcell.KeyEscape: - clean() - } - }) - t.flex.RemoveItem(t.errs) - t.flex.AddItem(t.command, 1, 0, true) - t.app.SetFocus(t.command) -} - -func (t *App) setFilter(s string) { - t.Frame.Selector = s - t.restart() -} - -func (t *App) actionNodeDaemonRestart(nodes map[string]any) { - ctx := context.Background() - for node, _ := range nodes { - _, _ = t.client.PostDaemonRestart(ctx, node) - } -} - -func (t *App) actionNodeDrain(nodes map[string]any) { - ctx := context.Background() - for node, _ := range nodes { - _, _ = t.client.PostPeerActionDrainWithResponse(ctx, node) - } -} - -func (t *App) actionNodeFreeze(nodes map[string]any) { - ctx := context.Background() - for node, _ := range nodes { - _, _ = t.client.PostPeerActionFreezeWithResponse(ctx, node, nil) - } -} - -func (t *App) actionNodeUnfreeze(nodes map[string]any) { - ctx := context.Background() - for node, _ := range nodes { - _, _ = t.client.PostPeerActionUnfreezeWithResponse(ctx, node, nil) - } -} - -func (t *App) actionAbort(paths map[string]any) { - ctx := context.Background() - for path := range paths { - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostObjectActionAbortWithResponse(ctx, p.Namespace, p.Kind, p.Name) - } -} - -func (t *App) actionRestart(paths map[string]any) { - ctx := context.Background() - for path := range paths { - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostObjectActionRestartWithResponse(ctx, p.Namespace, p.Kind, p.Name, api.PostObjectActionRestart{}) - } -} - -func (t *App) actionInstanceRestart(keys map[[2]string]any) { - ctx := context.Background() - for key := range keys { - path := key[0] - node := key[1] - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostInstanceActionRestartWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) - } -} - -func (t *App) actionInstanceStart(keys map[[2]string]any) { - ctx := context.Background() - for key := range keys { - path := key[0] - node := key[1] - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostInstanceActionStartWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) - } -} - -func (t *App) actionInstanceStop(keys map[[2]string]any) { - ctx := context.Background() - for key, _ := range keys { - path := key[0] - node := key[1] - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostInstanceActionStopWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) - } -} - -func (t *App) actionInstanceProvision(keys map[[2]string]any) { - ctx := context.Background() - for key, _ := range keys { - path := key[0] - node := key[1] - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostInstanceActionProvisionWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) - } -} - -func (t *App) actionInstanceUnprovision(keys map[[2]string]any) { - ctx := context.Background() - for key, _ := range keys { - path := key[0] - node := key[1] - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostInstanceActionUnprovisionWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) - } -} - -func (t *App) actionInstanceFreeze(keys map[[2]string]any) { - ctx := context.Background() - for key, _ := range keys { - path := key[0] - node := key[1] - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostInstanceActionFreezeWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) - } -} - -func (t *App) actionInstanceUnfreeze(keys map[[2]string]any) { - ctx := context.Background() - for key, _ := range keys { - path := key[0] - node := key[1] - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostInstanceActionUnfreezeWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) - } -} - -func (t *App) actionInstanceSwitch(keys map[[2]string]any) { - ctx := context.Background() - m := make(map[string][]string) - for key, _ := range keys { - path := key[0] - node := key[1] - if l, ok := m[path]; ok { - l = append(l, node) - m[path] = l - } else { - l = append([]string{}, node) - m[path] = l - } - } - for path, nodes := range m { - p, err := naming.ParsePath(path) - if err != nil { - continue - } - body := api.PostObjectActionSwitch{ - Destination: nodes, - } - _, _ = t.client.PostObjectActionSwitchWithResponse(ctx, p.Namespace, p.Kind, p.Name, body) - } -} - -func (t *App) actionStop(paths map[string]any) { - ctx := context.Background() - for path, _ := range paths { - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostObjectActionStopWithResponse(ctx, p.Namespace, p.Kind, p.Name) - } -} - -func (t *App) actionPurge(paths map[string]any) { - ctx := context.Background() - for path, _ := range paths { - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostObjectActionPurgeWithResponse(ctx, p.Namespace, p.Kind, p.Name) - } -} - -func (t *App) actionDelete(paths map[string]any) { - ctx := context.Background() - for path, _ := range paths { - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostObjectActionDeleteWithResponse(ctx, p.Namespace, p.Kind, p.Name) - } -} - -func (t *App) actionStart(paths map[string]any) { - ctx := context.Background() - for path, _ := range paths { - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostObjectActionStartWithResponse(ctx, p.Namespace, p.Kind, p.Name) - } -} - -func (t *App) actionProvision(paths map[string]any) { - ctx := context.Background() - for path, _ := range paths { - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostObjectActionProvisionWithResponse(ctx, p.Namespace, p.Kind, p.Name) - } -} - -func (t *App) actionUnprovision(paths map[string]any) { - ctx := context.Background() - for path, _ := range paths { - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostObjectActionUnprovisionWithResponse(ctx, p.Namespace, p.Kind, p.Name) - } -} - -func (t *App) actionFreeze(paths map[string]any) { - ctx := context.Background() - for path, _ := range paths { - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostObjectActionFreezeWithResponse(ctx, p.Namespace, p.Kind, p.Name) - } -} - -func (t *App) actionUnfreeze(paths map[string]any) { - ctx := context.Background() - for path, _ := range paths { - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostObjectActionUnfreezeWithResponse(ctx, p.Namespace, p.Kind, p.Name) - } -} - -func (t *App) actionSwitch(paths map[string]any) { - ctx := context.Background() - for path, _ := range paths { - p, err := naming.ParsePath(path) - if err != nil { - continue - } - body := api.PostObjectActionSwitch{} - _, _ = t.client.PostObjectActionSwitchWithResponse(ctx, p.Namespace, p.Kind, p.Name, body) - } -} - -func (t *App) actionGiveback(paths map[string]any) { - ctx := context.Background() - for path, _ := range paths { - p, err := naming.ParsePath(path) - if err != nil { - continue - } - _, _ = t.client.PostObjectActionGivebackWithResponse(ctx, p.Namespace, p.Kind, p.Name) - } -} - -func (t *App) actionClusterFreeze() { - ctx := context.Background() - _, _ = t.client.PostClusterActionFreezeWithResponse(ctx) -} - -func (t *App) actionClusterUnfreeze() { - ctx := context.Background() - _, _ = t.client.PostClusterActionUnfreezeWithResponse(ctx) -} - -func (t *App) onRuneH(event *tcell.EventKey) { - help := ` - Command mode Shortcuts - - : Enter command mode - ESC Exit command mode - Enter Apply command to the selected cells - - Selection Shortcuts - - Up,Right,Down,Left Move cursor - SPACE Select the cell - ESC Reset selection - Ctrl-a Invert object selection - - Misc Shortcuts - - c Show object configuration - h Show this help - l Show node, object or instance logs - q Quit - Enter Show instance status - ESC Close popup - - Cluster commands: - - freeze, unfreeze - - Object commands: - - abort, delete, freeze, giveback, provision, purge, start, stop, switch, - unfreeze, unprovision - - Instance commands: - - delete, freeze, provision, start, stop, switch, unfreeze, unprovision - - Node commands: - - drain freeze, unfreeze - -` - t.initTextView() - t.textView.SetTitle("Help") - fmt.Fprint(t.textView, help) -} - -func (t *App) onRuneS(event *tcell.EventKey) { -} - -func (t *App) onRuneL(event *tcell.EventKey) { - row, col := t.objects.GetSelection() - t.viewNode = "" - t.viewPath = naming.Path{} - if row >= t.firstObjectRow { - path := t.objects.GetCell(row, 0).Text - p, err := naming.ParsePath(path) - if err != nil { - return - } - t.viewPath = p - } - if col >= t.firstInstanceCol { - t.viewNode = t.objects.GetCell(t.firstObjectRow-1, col).Text - } - - title := func() string { - switch { - case !t.viewPath.IsZero() && t.viewNode != "": - return fmt.Sprintf("%s@%s log", t.viewPath, t.viewNode) - case !t.viewPath.IsZero(): - return fmt.Sprintf("%s log", t.viewPath) - case t.viewNode != "": - return fmt.Sprintf("%s log", t.viewNode) - default: - return "" - } - } - - t.initTextView() - t.nav(viewLog) - t.textView.SetDynamicColors(true) - t.textView.SetTitle(title()) - t.textView.SetChangedFunc(func() { - t.textView.ScrollToEnd() - }) - - t.textView.Clear() - - lines := 50 - follow := true - log := t.client.NewGetLogs(t.viewNode). - //SetFilters(nil). - SetLines(&lines). - SetFollow(&follow) - if !t.viewPath.IsZero() { - l := naming.Paths{t.viewPath}.StrSlice() - log.SetPaths(&l) - } - reader, err := log.GetReader() - if err != nil { - t.errorf("%s", err) - return - } - t.logCloser = reader - - w := zerolog.NewConsoleWriter() - w.Out = tview.ANSIWriter(t.textView) - w.TimeFormat = "2006-01-02T15:04:05.000Z07:00" - w.FormatFieldName = func(i any) string { return "" } - w.FormatFieldValue = func(i any) string { return "" } - w.FormatMessage = func(i any) string { - return rawconfig.Colorize.Bold(i) - } - - go func() { - for { - event, err := reader.Read() - if errors.Is(err, io.EOF) { - break - } else if errors.Is(err, context.Canceled) { - break - } else if err != nil { - t.errorf("%s", err) - break - } - rec, err := streamlog.NewEvent(event.Data) - if err != nil { - t.errorf("%s", err) - break - } - switch s := rec.M["JSON"].(type) { - case string: - _, _ = w.Write([]byte(s)) - } - } - }() -} - -func (t *App) onEnter(event *tcell.EventKey) { - row, col := t.objects.GetSelection() - t.viewPath = naming.Path{} - t.viewNode = "" - if row >= t.firstObjectRow { - path := t.objects.GetCell(row, 0).Text - p, err := naming.ParsePath(path) - if err != nil { - return - } - t.viewPath = p - } - if col >= t.firstInstanceCol { - node := t.objects.GetCell(t.firstObjectRow-1, col).Text - t.viewNode = node - } - switch { - case !t.viewPath.IsZero() && t.viewNode != "": - t.initTextView() - t.nav(viewInstance) - case t.viewPath.Kind == naming.KindCfg || t.viewPath.Kind == naming.KindSec: - t.nav(viewKeys) - } -} - -func (t *App) setFocus() { - switch t.focus() { - case viewConfig: - t.app.SetFocus(t.textView) - case viewInstance: - t.app.SetFocus(t.textView) - case viewLog: - t.app.SetFocus(t.textView) - case viewKeys: - t.app.SetFocus(t.keys) - default: - t.app.SetFocus(t.objects) - } -} - -func (t *App) updateKeysView() { - if t.viewPath.IsZero() { - return - } - t.initKeysTable() - resp, err := t.client.GetObjectKVStoreKeysWithResponse(context.Background(), t.viewPath.Namespace, t.viewPath.Kind, t.viewPath.Name) - if err != nil { - return - } - if resp.StatusCode() != http.StatusOK { - return - } - t.keys.Clear() - t.keys.SetCell(0, 0, tview.NewTableCell("NAME").SetTextColor(colorTitle)) - t.keys.SetCell(0, 1, tview.NewTableCell("SIZE").SetTextColor(colorTitle)) - for i, key := range resp.JSON200.Items { - row := 1 + i - t.keys.SetCell(row, 0, tview.NewTableCell(key.Key).SetSelectable(true)) - t.keys.SetCell(row, 1, tview.NewTableCell(sizeconv.BSizeCompact(float64(key.Size)))) - } -} - -func (t *App) updateInstanceView() { - digest := t.Frame.Current.GetObjectStatus(t.viewPath) - text := tview.TranslateANSI(digest.Render([]string{t.viewNode})) - t.initTextView() - title := fmt.Sprintf("%s@%s status", t.viewPath, t.viewNode) - t.textView.SetDynamicColors(true) - t.textView.SetTitle(title) - t.textView.Clear() - fmt.Fprint(t.textView, text) -} - -func (t *App) onRuneC(event *tcell.EventKey) { - row, _ := t.objects.GetSelection() - path := t.objects.GetCell(row, 0).Text - p, err := naming.ParsePath(path) - if err != nil { - return - } - t.viewPath = p - t.viewNode = "" - t.initTextView() - t.updateConfigView() - t.nav(viewConfig) -} - -func (t *App) updateConfigView() { - if t.viewPath.IsZero() { - return - } - resp, err := t.client.GetObjectConfigFileWithResponse(context.Background(), t.viewPath.Namespace, t.viewPath.Kind, t.viewPath.Name) - if err != nil { - return - } - if resp.StatusCode() != http.StatusOK { - return - } - - text := tview.TranslateANSI(string(resp.Body)) - title := fmt.Sprintf("%s configuration", t.viewPath) - t.textView.SetDynamicColors(false) - t.textView.SetTitle(title) - t.textView.Clear() - fmt.Fprint(t.textView, text) -} - -func (t *App) infof(format string, args ...any) { - t.printf(tcell.ColorGray, format, args...) -} - -func (t *App) warnf(format string, args ...any) { - t.printf(tcell.ColorOrange, format, args...) -} - -func (t *App) errorf(format string, args ...any) { - t.printf(tcell.ColorRed, format, args...) -} - -func (t *App) printf(color tcell.Color, format string, args ...any) { - t.flex.AddItem(t.errs, 1, 0, false) - t.errs.Clear() - t.errs.SetBackgroundColor(color) - fmt.Fprintf(t.errs, format, args...) - time.AfterFunc(5*time.Second, func() { - t.flex.RemoveItem(t.errs) - }) -} - -func (t *App) nav(to viewId) { - from := t.focus() - t.push(to) - if to == from { - return - } - t.navFromTo(from, to) -} - -func (t *App) back() { - from := t.pop() - to := t.focus() - if to == from { - return - } - t.navFromTo(from, to) -} - -func (t *App) navFromTo(from, to viewId) { - t.flex.Clear() - switch from { - case viewObject: - case viewLog: - t.textView.SetChangedFunc(nil) - t.textView = nil - t.logCloser.Close() - case viewConfig, viewInstance, viewKey: - t.textView = nil - case viewKeys: - t.keys = nil - } - switch to { - case viewLog, viewConfig, viewKey: - t.flex.AddItem(t.textView, 0, 1, true) - t.app.SetFocus(t.textView) - case viewInstance: - t.flex.AddItem(t.textView, 0, 1, true) - t.app.SetFocus(t.textView) - t.updateInstanceView() - case viewKeys: - t.updateKeysView() - t.flex.AddItem(t.keys, 0, 1, true) - t.app.SetFocus(t.keys) - case viewObject: - t.flex.AddItem(t.objects, 0, 1, true) - t.app.SetFocus(t.objects) - t.updateObjects() - } - t.flex.AddItem(t.errs, 1, 0, false) -} - -func (t *App) initTextView() { - if t.textView != nil { - return - } - v := tview.NewTextView() - v.SetScrollable(true) - v.SetBorder(true) - v.SetBorderPadding(1, 1, 1, 1) - v.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { - case tcell.KeyESC: - t.back() - } - switch event.Rune() { - case 'q': - t.stop() - case ':': - t.onRuneColumn(event) - default: - return event - } - return nil - }) - - t.textView = v - return -} - -func (t *App) restart() { - t.restartC <- nil -} - -func (t *App) stop() { - t.exitFlag.Store(true) - t.errC <- nil - t.app.Stop() -} diff --git a/core/ox/root.go b/core/ox/root.go index 49a4b3c19..3fd4e0a97 100644 --- a/core/ox/root.go +++ b/core/ox/root.go @@ -14,6 +14,7 @@ import ( "github.com/opensvc/om3/core/env" "github.com/opensvc/om3/core/naming" "github.com/opensvc/om3/core/rawconfig" + "github.com/opensvc/om3/core/tui" "github.com/opensvc/om3/util/version" ) @@ -35,6 +36,9 @@ var ( ValidArgsFunction: validArgs, BashCompletionFunction: bashCompletionFunction, Version: version.Version(), + RunE: func(cmd *cobra.Command, args []string) error { + return tui.Run(args) + }, } ) diff --git a/core/oxcmd/lib_remote_config.go b/core/oxcmd/lib_remote_config.go index 4aac2aa55..0a7befc3f 100644 --- a/core/oxcmd/lib_remote_config.go +++ b/core/oxcmd/lib_remote_config.go @@ -16,9 +16,6 @@ func createTempRemoteConfig(p naming.Path, c *client.T) (string, error) { buff []byte f *os.File ) - if c, err = remoteClient(p, c); err != nil { - return "", err - } if buff, err = fetchConfig(p, c); err != nil { return "", err } @@ -33,28 +30,6 @@ func createTempRemoteConfig(p naming.Path, c *client.T) (string, error) { return filename, nil } -func remoteClient(p naming.Path, c *client.T) (*client.T, error) { - resp, err := c.GetObjectWithResponse(context.Background(), p.Namespace, p.Kind, p.Name) - if err != nil { - return nil, err - } - if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf("get object %s data from %s: %s", p, c.URL(), resp.Status()) - } - var nodename string - for k := range resp.JSON200.Data.Instances { - nodename = k - break - } - if nodename == "" { - return nil, fmt.Errorf("%s has no instance", p) - } - if c, err = client.New(client.WithURL(nodename)); err != nil { - return nil, err - } - return c, nil -} - func fetchConfig(p naming.Path, c *client.T) ([]byte, error) { resp, err := c.GetObjectConfigFileWithResponse(context.Background(), p.Namespace, p.Kind, p.Name) if err != nil { diff --git a/core/oxcmd/object_edit_config.go b/core/oxcmd/object_edit_config.go index fd932980f..bea05dfc9 100644 --- a/core/oxcmd/object_edit_config.go +++ b/core/oxcmd/object_edit_config.go @@ -29,14 +29,14 @@ func (t *CmdObjectEditConfig) do(selector string, c *client.T) error { return err } for _, p := range paths { - if err := t.doRemote(p, c); err != nil { + if err := t.DoRemote(p, c); err != nil { return err } } return nil } -func (t *CmdObjectEditConfig) doRemote(p naming.Path, c *client.T) error { +func (t *CmdObjectEditConfig) DoRemote(p naming.Path, c *client.T) error { var ( err error refSum []byte diff --git a/core/oxcmd/object_edit_key.go b/core/oxcmd/object_edit_key.go index 88a9a9d1d..b1337d9e1 100644 --- a/core/oxcmd/object_edit_key.go +++ b/core/oxcmd/object_edit_key.go @@ -32,7 +32,7 @@ func (t *CmdObjectEditKey) do(selector string, c *client.T) error { return err } for _, p := range paths { - if err := t.doRemote(p, c); err != nil { + if err := t.DoRemote(p, c); err != nil { return err } } @@ -72,7 +72,7 @@ func pushKey(p naming.Path, key string, fName string, c *client.T) (err error) { return nil } -func (t *CmdObjectEditKey) doRemote(p naming.Path, c *client.T) error { +func (t *CmdObjectEditKey) DoRemote(p naming.Path, c *client.T) error { var ( err error refSum []byte From ad1a910758f0b49afcb1d33ffd0813e59108fbd9 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Thu, 24 Oct 2024 11:14:33 +0200 Subject: [PATCH 10/17] Add the core/tui package --- core/tui/main.go | 1683 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1683 insertions(+) create mode 100644 core/tui/main.go diff --git a/core/tui/main.go b/core/tui/main.go new file mode 100644 index 000000000..5863ec779 --- /dev/null +++ b/core/tui/main.go @@ -0,0 +1,1683 @@ +package tui + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "sort" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/gdamore/tcell/v2" + "github.com/opensvc/om3/core/client" + "github.com/opensvc/om3/core/clusterdump" + "github.com/opensvc/om3/core/event" + "github.com/opensvc/om3/core/monitor" + "github.com/opensvc/om3/core/naming" + "github.com/opensvc/om3/core/oxcmd" + "github.com/opensvc/om3/core/rawconfig" + "github.com/opensvc/om3/core/streamlog" + "github.com/opensvc/om3/daemon/api" + "github.com/opensvc/om3/daemon/msgbus" + "github.com/opensvc/om3/util/sizeconv" + "github.com/rivo/tview" + "github.com/rs/zerolog" +) + +type ( + viewId int + viewStack []viewId + + App struct { + *monitor.Frame + + eventCount uint64 + + stack viewStack + + app *tview.Application + top *tview.TextView + head *tview.TextView + errs *tview.TextView + textView *tview.TextView + keys *tview.Table + objects *tview.Table + flex *tview.Flex + command *tview.InputField + + client *client.T + + lastDraw time.Time + + viewPath naming.Path + viewNode string + viewKey string + + firstInstanceCol int + firstObjectRow int + + maxRetries int + displayInterval time.Duration + + selectedNodes map[string]any + selectedPaths map[string]any + selectedInstances map[[2]string]any + + errC chan error + restartC chan error + exitFlag atomic.Bool + + logCloser io.Closer + } + + getter interface { + Get() ([]byte, error) + } +) + +const ( + viewObject viewId = iota + viewConfig + viewKey + viewKeys + viewInstance + viewLog + viewLast // marker, not a real view +) + +var ( + colorNone = tcell.ColorNone + colorSelected = tcell.ColorDarkSlateGray + colorTitle = tcell.ColorGray + colorHead = tcell.ColorYellow + colorHighlight = tcell.ColorWhite + + spin = []rune{'⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'} + spinLen = len(spin) +) + +func Run(args []string) error { + app := NewApp() + if len(args) > 0 { + app.Frame.Selector = args[1] + } + return NewApp().Run() +} + +func (t viewStack) String() string { + l := []string{ + viewObject.String(), + } + for _, v := range t { + l = append(l, v.String()) + } + return strings.Join(l, " > ") +} + +func (t *App) updateHead() { + s := t.stack.String() + if t.focus() == viewObject { + s += " : " + t.Frame.Selector + } else if more := t.selectedString(); more != "" { + s += " : " + more + } + t.head.SetText(s) +} + +func (t viewId) String() string { + switch t { + case viewObject: + return "objects" + case viewConfig: + return "configuration" + case viewKey: + return "key" + case viewKeys: + return "keys" + case viewInstance: + return "instance" + case viewLog: + return "log" + default: + return "" + } +} + +func (t *App) push(v viewId) { + t.stack = append(t.stack, v) +} + +func (t *App) pop() viewId { + n := len(t.stack) + if n == 0 { + return viewObject + } + v := t.stack[n-1] + t.stack = t.stack[:n-1] + return v +} + +func (t *App) focus() viewId { + n := len(t.stack) + if n == 0 { + return viewObject + } + return t.stack[n-1] +} + +func NewApp() *App { + return &App{ + stack: make([]viewId, 0), + firstInstanceCol: 5, + maxRetries: 600, + displayInterval: 500 * time.Millisecond, + Frame: &monitor.Frame{ + Selector: "*/svc/*", + Sections: []string{}, + }, + selectedNodes: make(map[string]any), + selectedPaths: make(map[string]any), + selectedInstances: make(map[[2]string]any), + errC: make(chan error), + restartC: make(chan error), + } +} + +func (t *App) resetAllSelected() { + t.resetSelectedNodes() + t.resetSelectedPaths() + t.resetSelectedInstances() +} + +func (t *App) updateKeyTextView() { + if t.viewKey == "" { + return + } + resp, err := t.client.GetObjectKVStoreEntryWithResponse(context.Background(), t.viewPath.Namespace, t.viewPath.Kind, t.viewPath.Name, &api.GetObjectKVStoreEntryParams{ + Key: t.viewKey, + }) + if err != nil { + t.errorf("%s", err) + return + } + if resp.StatusCode() != http.StatusOK { + t.errorf("status code: %s", resp.Status()) + return + } + + t.initTextView() + text := string(resp.Body) + title := fmt.Sprintf("%s key %s", t.viewPath, t.viewKey) + t.textView.SetTitle(title) + t.textView.Clear() + fmt.Fprint(t.textView, text) +} + +func (t *App) initKeysTable() { + table := tview.NewTable() + table.SetBorder(false) + + onEnter := func(event *tcell.EventKey) { + t.updateKeyTextView() + t.nav(viewKey) + } + + table.SetSelectionChangedFunc(func(row, col int) { + t.viewKey = "" + if row == 0 { + return + } + if col == 0 { + t.viewKey = table.GetCell(row, col).Text + } + + }) + table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyLeft, tcell.KeyRight, tcell.KeyUp, tcell.KeyDown: + table.SetSelectable(true, false) + case tcell.KeyEnter: + onEnter(event) + return nil // prevents the default select behaviour + } + return event + }) + t.keys = table +} + +func (t *App) initObjectsTable() { + table := tview.NewTable() + table.SetEvaluateAllRows(true) + + selectedFunc := func(row, col int) { + cell := table.GetCell(row, col) + path := table.GetCell(row, 0).Text + node := table.GetCell(0, col).Text + var selected *bool + switch { + case row == 0 && col >= t.firstInstanceCol: + v := t.toggleNode(node) + selected = &v + case row < t.firstObjectRow: + case col == 0: + v := t.togglePath(path) + selected = &v + case col >= t.firstInstanceCol: + v := t.toggleInstance(path, node) + selected = &v + } + if selected != nil && *selected { + cell.SetBackgroundColor(colorSelected) + } else { + cell.SetBackgroundColor(colorNone) + } + } + + table.SetSelectedFunc(selectedFunc) + + setSelection := func(table *tview.Table) { + row, col := table.GetSelection() + cell := table.GetCell(row, col) + cell.SetBackgroundColor(colorSelected) + table.SetCell(row, col, cell) + selectedFunc(row, col) + } + + selectAll := func() { + for i := t.firstObjectRow; i < table.GetRowCount(); i++ { + selectedFunc(i, 0) + } + } + + table.SetSelectionChangedFunc(func(row, col int) { + t.viewNode = "" + t.viewPath = naming.Path{} + if row >= t.firstObjectRow { + path := t.objects.GetCell(row, 0).Text + p, err := naming.ParsePath(path) + if err != nil { + return + } + t.viewPath = p + } + if col >= t.firstInstanceCol { + t.viewNode = t.objects.GetCell(0, col).Text + } + }) + table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyLeft, tcell.KeyRight, tcell.KeyUp, tcell.KeyDown: + table.SetSelectable(true, true) + case tcell.KeyESC: + t.resetSelectedNodes() + t.resetSelectedPaths() + t.resetSelectedInstances() + case tcell.KeyCtrlA: + selectAll() + case tcell.KeyEnter: + t.onEnter(event) + return nil // prevents the default select behaviour + } + switch event.Rune() { + case ' ': + setSelection(table) + } + return event + }) + t.objects = table +} + +func (t *App) initHeadTextView() { + t.head = tview.NewTextView() + t.head.SetBorder(false) +} + +func (t *App) initErrsTextView() { + t.errs = tview.NewTextView() + t.errs.SetBorder(false) +} + +func (t *App) viewPrimitive(v viewId) tview.Primitive { + switch v { + case viewConfig, viewInstance, viewKey, viewLog: + return t.textView + case viewKeys: + return t.keys + default: + return t.objects + } +} + +func (t *App) initApp() { + t.initHeadTextView() + t.initObjectsTable() + t.initErrsTextView() + + t.app = tview.NewApplication() + t.flex = tview.NewFlex().SetDirection(tview.FlexRow) + t.flex.AddItem(t.head, 1, 0, false) + t.head.SetBackgroundColor(colorHead) + t.updateHead() + t.flex.AddItem(t.objects, 0, 1, true) + t.app.SetRoot(t.flex, true) + + t.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyESC: + t.back() + case tcell.KeyTab: + v := t.focus() + v++ + if v >= viewLast { + v = 0 + } + if primitive := t.viewPrimitive(v); primitive != nil { + if primitive == nil { + t.infof("xx no primitive for %d", v) + } + //t.app.SetFocus(primitive) + } + return nil + } + if t.command != nil { + return event + } + switch event.Rune() { + case ':': + t.onRuneColumn(event) + return nil + case 'c': + t.onRuneC(event) + case 'e': + t.onRuneE(event) + case 'h': + t.onRuneH(event) + case 'l': + t.onRuneL(event) + case 'q': + t.stop() + } + return event + }) +} + +func (t *App) init() error { + t.initApp() + + if cli, err := client.New(client.WithTimeout(0)); err != nil { + return err + } else { + t.client = cli + } + + monitor.InitColor() + + return nil +} + +func (t *App) Run() error { + if err := t.init(); err != nil { + return err + } + go t.runEventReader() + return t.app.Run() +} + +func (t *App) runEventReader() { + for { + evReader, err := t.client.NewGetEvents().SetSelector(t.Selector).GetReader() + if err != nil { + t.errorf("%s", err) + if t.exitFlag.Load() { + return + } + time.Sleep(10 * time.Millisecond) + continue + } + + statusGetter := t.client.NewGetDaemonStatus().SetSelector(t.Selector) + err = t.do(statusGetter, evReader) + _ = evReader.Close() + if t.exitFlag.Load() { + return + } + if err != nil { + time.Sleep(10 * time.Millisecond) + } + } +} + +func (t *App) do(statusGetter getter, evReader event.ReadCloser) error { + var ( + b []byte + data *clusterdump.Data + err error + + eventC = make(chan event.Event, 100) + dataC = make(chan *clusterdump.Data) + + nextEventID uint64 + + wg = sync.WaitGroup{} + ) + + defer wg.Wait() + + wg.Add(1) + go func() { + defer wg.Done() + defer close(eventC) + + for { + ev, err := evReader.Read() + if err != nil { + err = fmt.Errorf("event queue read error: %w", err) + t.errorf("%s", err) + t.errC <- err + return + } + eventC <- *ev + } + }() + + //t.infof("get daemon status") + b, err = statusGetter.Get() + if err != nil { + return err + } + if err := json.Unmarshal(b, &data); err != nil { + return err + } + + cdata := msgbus.NewClusterData(data) + wg.Add(1) + go func(d *clusterdump.Data) { + defer wg.Done() + t.Current = *d + t.Nodename = data.Daemon.Nodename + t.app.QueueUpdateDraw(func() { + t.updateObjects() + }) + // show data when new data published on dataC + for d := range dataC { + t.Current = *d + t.Nodename = data.Daemon.Nodename + t.eventCount++ + t.app.QueueUpdateDraw(func() { + // TODO: detect if t.updateInstanceView and t.updateConfigView need to be called (config mtime change, ...) + switch t.focus() { + case viewInstance: + t.updateInstanceView() + case viewConfig: + t.updateConfigView() + case viewKeys: + t.updateKeysView() + default: + t.updateObjects() + } + }) + } + }(data.DeepCopy()) + + defer close(dataC) + + ticker := time.NewTicker(t.displayInterval) + defer ticker.Stop() + changes := false + for { + select { + case <-t.restartC: + _ = evReader.Close() + case err := <-t.errC: + return err + case e := <-eventC: + if nextEventID == 0 { + nextEventID = e.ID + } else if e.ID != nextEventID { + err := fmt.Errorf("broken event chain: received event id %d, expected %d", e.ID, nextEventID) + t.errorf("%s", err) + return err + } + nextEventID++ + changes = true + msg, err := msgbus.EventToMessage(e) + if err != nil { + t.errorf("EventToMessage event id %d %s error: %s", e.ID, e.Kind, err) + continue + } + cdata.ApplyMessage(msg) + case <-ticker.C: + if changes { + dataC <- cdata.DeepCopy() + changes = false + } else if t.focus() == viewObject { + t.app.QueueUpdateDraw(func() { + s := fmt.Sprint(time.Now().Truncate(time.Second).Sub(t.lastDraw.Truncate(time.Second))) + t.objects.SetCell(2, 1, tview.NewTableCell(s).SetSelectable(false)) + }) + } + } + } +} + +func (t *App) paths() []string { + paths := make([]string, len(t.Current.Cluster.Object)) + i := 0 + for path := range t.Current.Cluster.Object { + paths[i] = path + i += 1 + } + sort.Strings(paths) + return paths + +} + +func (t *App) updateObjects() { + nodesCells := func(row int, selectable bool) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + t.objects.SetCell(row, t.firstInstanceCol+i, t.cellNode(nodename, selectable)) + } + } + + nodesScoreCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeScore(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + nodesLoadCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeLoad(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + nodesMemCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeMem(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + nodesSwapCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeSwap(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + nodesStateCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeStates(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + t.lastDraw = time.Now() + + t.objects.Clear() + + row := 0 + t.objects.SetCell(row, 0, tview.NewTableCell("CLUSTER").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell(t.Current.Cluster.Config.Name).SetSelectable(true)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("NODE").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesCells(row, true) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("EVENT").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell(fmt.Sprintf("%d", t.eventCount)).SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("SCORE").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesScoreCells(row) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("LAST").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("0s").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell(".LOAD").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesLoadCells(row) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell(".MEM").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesMemCells(row) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell(".SWAP").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesSwapCells(row) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("STATE").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesStateCells(row) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("┼").SetTextColor(colorTitle).SetSelectable(false)) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("PATH").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("AVAIL").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("ORCHESTRATE").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("UP").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesCells(row, false) + + t.firstObjectRow = row + 1 + + t.objects.SetFixed(t.firstObjectRow, 2) + + for _, path := range t.paths() { + row++ + t.objects.SetCell(row, 0, t.cellObjectPath(path)) + t.objects.SetCell(row, 1, t.cellObjectStatus(path)) + t.objects.SetCell(row, 2, t.cellObjectOrchestrate(path)) + t.objects.SetCell(row, 3, t.cellObjectRunning(path)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + for j, nodename := range t.Current.Cluster.Config.Nodes { + t.objects.SetCell(row, 5+j, t.cellInstanceStatus(path, nodename)) + } + } +} + +func (t *App) cellObjectOrchestrate(path string) *tview.TableCell { + s := t.Current.Cluster.Object[path].Orchestrate + return tview.NewTableCell(s).SetSelectable(false) +} + +func (t *App) cellObjectRunning(path string) *tview.TableCell { + s := tview.TranslateANSI(t.StrObjectRunning(path)) + return tview.NewTableCell(s).SetSelectable(false) +} + +func (t *App) cellObjectStatus(path string) *tview.TableCell { + s := tview.TranslateANSI(monitor.StrObjectStatus(t.Current.Cluster.Object[path])) + return tview.NewTableCell(s).SetSelectable(false) +} + +func (t *App) cellInstanceStatus(path, node string) *tview.TableCell { + s := tview.TranslateANSI(t.StrObjectInstance(path, node, t.Current.Cluster.Object[path].Scope)) + cell := tview.NewTableCell(s) + if t.isInstanceSelected(path, node) { + cell.SetBackgroundColor(colorSelected) + } + return cell +} + +func (t *App) cellNode(node string, selectable bool) *tview.TableCell { + cell := tview.NewTableCell(node).SetAttributes(tcell.AttrBold).SetSelectable(selectable) + if selectable && t.isNodeSelected(node) { + cell.SetBackgroundColor(colorSelected) + } + return cell +} + +func (t *App) cellObjectPath(path string) *tview.TableCell { + cell := tview.NewTableCell(path).SetAttributes(tcell.AttrBold) + if t.isPathSelected(path) { + cell.SetBackgroundColor(colorSelected) + } + return cell +} + +func (t *App) toggleInstance(path, node string) bool { + key := [2]string{path, node} + if _, ok := t.selectedInstances[key]; ok { + delete(t.selectedInstances, key) + return false + } else { + t.selectedInstances[key] = nil + t.resetSelectedPaths() + t.resetSelectedNodes() + return true + } +} + +func (t *App) togglePath(key string) bool { + if _, ok := t.selectedPaths[key]; ok { + delete(t.selectedPaths, key) + return false + } else { + t.selectedPaths[key] = nil + t.resetSelectedInstances() + t.resetSelectedNodes() + return true + } +} + +func (t *App) toggleNode(key string) bool { + if _, ok := t.selectedNodes[key]; ok { + delete(t.selectedNodes, key) + return false + } else { + t.selectedNodes[key] = nil + t.resetSelectedInstances() + t.resetSelectedPaths() + return true + } +} + +func (t *App) resetSelectedNodes() { + if len(t.selectedNodes) == 0 { + return + } + t.selectedNodes = make(map[string]any) + for j := t.firstInstanceCol; j < t.objects.GetColumnCount(); j += 1 { + t.objects.GetCell(0, j).SetBackgroundColor(colorNone) + } +} + +func (t *App) resetSelectedInstances() { + if len(t.selectedInstances) == 0 { + return + } + t.selectedInstances = make(map[[2]string]any) + for i := 1; i < t.objects.GetRowCount(); i += 1 { + for j := t.firstInstanceCol; j < t.objects.GetColumnCount(); j += 1 { + t.objects.GetCell(i, j).SetBackgroundColor(colorNone) + } + } +} + +func (t *App) resetSelectedPaths() { + if len(t.selectedPaths) == 0 { + return + } + t.selectedPaths = make(map[string]any) + for i := 1; i < t.objects.GetRowCount(); i += 1 { + t.objects.GetCell(i, 0).SetBackgroundColor(colorNone) + } +} + +func (t *App) isInstanceSelected(path, node string) bool { + _, ok := t.selectedInstances[[2]string{path, node}] + return ok +} + +func (t *App) isPathSelected(path string) bool { + _, ok := t.selectedPaths[path] + return ok +} + +func (t *App) isNodeSelected(node string) bool { + _, ok := t.selectedNodes[node] + return ok +} + +func (t *App) onRuneColumn(event *tcell.EventKey) { + clean := func() { + t.flex.RemoveItem(t.command) + t.command = nil + t.setFocus() + } + if t.command != nil { + clean() + return + } + clusterAction := func(action string) { + switch action { + case "freeze": + t.actionClusterFreeze() + case "unfreeze", "thaw": + t.actionClusterUnfreeze() + } + } + objectAction := func(action string, paths map[string]any) { + switch action { + case "stop": + t.actionStop(paths) + case "start": + t.actionStart(paths) + case "provision": + t.actionProvision(paths) + case "unprovision": + t.actionUnprovision(paths) + case "freeze": + t.actionFreeze(paths) + case "unfreeze", "thaw": + t.actionUnfreeze(paths) + case "switch": + t.actionSwitch(paths) + case "giveback": + t.actionGiveback(paths) + case "abort": + t.actionAbort(paths) + case "purge": + t.actionPurge(paths) + case "delete": + t.actionDelete(paths) + case "restart": + t.actionRestart(paths) + default: + t.errorf("unknown command: %s", action) + } + } + instanceAction := func(action string, keys map[[2]string]any) { + switch action { + case "stop": + t.actionInstanceStop(keys) + case "start": + t.actionInstanceStart(keys) + case "provision": + t.actionInstanceProvision(keys) + case "unprovision": + t.actionInstanceUnprovision(keys) + case "freeze": + t.actionInstanceFreeze(keys) + case "unfreeze", "thaw": + t.actionInstanceUnfreeze(keys) + case "restart": + t.actionInstanceRestart(keys) + case "switch": + t.actionInstanceSwitch(keys) + // case "clear": + // t.actionInstanceClear(keys) + default: + t.errorf("unknown command: %s", action) + } + } + nodeAction := func(action string, nodes map[string]any) { + switch action { + case "daemon restart": + t.actionNodeDaemonRestart(nodes) + case "freeze": + t.actionNodeFreeze(nodes) + case "unfreeze", "thaw": + t.actionNodeUnfreeze(nodes) + case "drain": + t.actionNodeDrain(nodes) + default: + t.errorf("unknown command: %s", action) + } + } + t.command = tview.NewInputField(). + SetLabel(":"). + SetFieldWidth(0). + SetDoneFunc(func(key tcell.Key) { + text := strings.TrimSpace(t.command.GetText()) + args := strings.Fields(text) + if len(args) == 0 { + clean() + return + } + action := args[0] + + switch key { + case tcell.KeyEnter: + switch action { + case "quit", "q": + t.stop() + case "filter": + if len(args) < 2 { + t.errorf("not enough arguments: filter ") + return + } + t.setFilter(args[1]) + clean() + return + case "go": + if len(args) < 2 { + t.errorf("not enough arguments: go ") + return + } + switch args[1] { + case "sec": + t.setFilter("*/sec/*") + clean() + return + case "cfg": + t.setFilter("*/cfg/*") + clean() + return + case "usr": + t.setFilter("*/usr/*") + clean() + return + case "svc": + t.setFilter("*/svc/*") + clean() + return + case "vol": + t.setFilter("*/vol/*") + clean() + return + } + case "do": + if len(args) < 2 { + t.errorf("not enough arguments: do ") + return + } + action = args[1] + switch { + case len(t.selectedPaths) > 0: + objectAction(action, t.selectedPaths) + case len(t.selectedInstances) > 0: + instanceAction(action, t.selectedInstances) + case len(t.selectedNodes) > 0: + nodeAction(action, t.selectedNodes) + default: + row, col := t.objects.GetSelection() + switch { + case row == 0 && col == 1: + clusterAction(action) + case row == 0 && col >= t.firstInstanceCol: + node := t.objects.GetCell(row, col).Text + selection := make(map[string]any) + selection[node] = nil + nodeAction(action, selection) + case row >= t.firstObjectRow && col == 0: + path := t.objects.GetCell(row, 0).Text + selection := make(map[string]any) + selection[path] = nil + objectAction(action, selection) + case row >= t.firstObjectRow && col >= t.firstInstanceCol: + path := t.objects.GetCell(row, 0).Text + node := t.objects.GetCell(0, col).Text + selection := make(map[[2]string]any) + selection[[2]string{path, node}] = nil + instanceAction(action, selection) + } + } + clean() + } + case tcell.KeyEscape: + clean() + } + }) + t.flex.RemoveItem(t.errs) + t.flex.AddItem(t.command, 1, 0, true) + t.app.SetFocus(t.command) +} + +func (t *App) setFilter(s string) { + t.Frame.Selector = s + t.updateHead() + t.restart() +} + +func (t *App) actionNodeDaemonRestart(nodes map[string]any) { + ctx := context.Background() + for node, _ := range nodes { + _, _ = t.client.PostDaemonRestart(ctx, node) + } +} + +func (t *App) actionNodeDrain(nodes map[string]any) { + ctx := context.Background() + for node, _ := range nodes { + _, _ = t.client.PostPeerActionDrainWithResponse(ctx, node) + } +} + +func (t *App) actionNodeFreeze(nodes map[string]any) { + ctx := context.Background() + for node, _ := range nodes { + _, _ = t.client.PostPeerActionFreezeWithResponse(ctx, node, nil) + } +} + +func (t *App) actionNodeUnfreeze(nodes map[string]any) { + ctx := context.Background() + for node, _ := range nodes { + _, _ = t.client.PostPeerActionUnfreezeWithResponse(ctx, node, nil) + } +} + +func (t *App) actionAbort(paths map[string]any) { + ctx := context.Background() + for path := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionAbortWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionRestart(paths map[string]any) { + ctx := context.Background() + for path := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionRestartWithResponse(ctx, p.Namespace, p.Kind, p.Name, api.PostObjectActionRestart{}) + } +} + +func (t *App) actionInstanceRestart(keys map[[2]string]any) { + ctx := context.Background() + for key := range keys { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostInstanceActionRestartWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) + } +} + +func (t *App) actionInstanceStart(keys map[[2]string]any) { + ctx := context.Background() + for key := range keys { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostInstanceActionStartWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) + } +} + +func (t *App) actionInstanceStop(keys map[[2]string]any) { + ctx := context.Background() + for key, _ := range keys { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostInstanceActionStopWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) + } +} + +func (t *App) actionInstanceProvision(keys map[[2]string]any) { + ctx := context.Background() + for key, _ := range keys { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostInstanceActionProvisionWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) + } +} + +func (t *App) actionInstanceUnprovision(keys map[[2]string]any) { + ctx := context.Background() + for key, _ := range keys { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostInstanceActionUnprovisionWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) + } +} + +func (t *App) actionInstanceFreeze(keys map[[2]string]any) { + ctx := context.Background() + for key, _ := range keys { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostInstanceActionFreezeWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) + } +} + +func (t *App) actionInstanceUnfreeze(keys map[[2]string]any) { + ctx := context.Background() + for key, _ := range keys { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostInstanceActionUnfreezeWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, nil) + } +} + +func (t *App) actionInstanceSwitch(keys map[[2]string]any) { + ctx := context.Background() + m := make(map[string][]string) + for key, _ := range keys { + path := key[0] + node := key[1] + if l, ok := m[path]; ok { + l = append(l, node) + m[path] = l + } else { + l = append([]string{}, node) + m[path] = l + } + } + for path, nodes := range m { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + body := api.PostObjectActionSwitch{ + Destination: nodes, + } + _, _ = t.client.PostObjectActionSwitchWithResponse(ctx, p.Namespace, p.Kind, p.Name, body) + } +} + +func (t *App) actionStop(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionStopWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionPurge(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionPurgeWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionDelete(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionDeleteWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionStart(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionStartWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionProvision(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionProvisionWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionUnprovision(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionUnprovisionWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionFreeze(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionFreezeWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionUnfreeze(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionUnfreezeWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionSwitch(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + body := api.PostObjectActionSwitch{} + _, _ = t.client.PostObjectActionSwitchWithResponse(ctx, p.Namespace, p.Kind, p.Name, body) + } +} + +func (t *App) actionGiveback(paths map[string]any) { + ctx := context.Background() + for path, _ := range paths { + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostObjectActionGivebackWithResponse(ctx, p.Namespace, p.Kind, p.Name) + } +} + +func (t *App) actionClusterFreeze() { + ctx := context.Background() + _, _ = t.client.PostClusterActionFreezeWithResponse(ctx) +} + +func (t *App) actionClusterUnfreeze() { + ctx := context.Background() + _, _ = t.client.PostClusterActionUnfreezeWithResponse(ctx) +} + +func (t *App) onRuneH(event *tcell.EventKey) { + help := ` + Command mode Shortcuts + + : Enter command mode + ESC Exit command mode + Enter Apply command to the selected cells + + Selection Shortcuts + + Up,Right,Down,Left Move cursor + SPACE Select the cell + ESC Reset selection + Ctrl-a Invert object selection + + Misc Shortcuts + + c Show object configuration + h Show this help + l Show node, object or instance logs + q Quit + Enter Show instance status + ESC Close popup + + Commands: + + do + + cluster actions: + freeze, unfreeze + + object actions: + abort, delete, freeze, giveback, provision, purge, start, stop, switch, + unfreeze, unprovision + + instance actions: + delete, freeze, provision, start, stop, switch, unfreeze, unprovision + + node actions: + drain freeze, unfreeze + + go + + sec, cfg, vol + + filter +` + savedItem := t.flex.GetItem(1) + savedFocus := t.app.GetFocus() + + v := tview.NewTextView(). + SetScrollable(true). + SetDynamicColors(true). + SetText(help) + v.SetBorder(true). + SetTitle("Help"). + SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyESC: + t.flex.RemoveItem(v) + t.flex.AddItem(t.head, 1, 0, false) + t.flex.AddItem(savedItem, 0, 1, true) + t.app.SetFocus(savedFocus) + } + return event + }) + t.flex.Clear() + t.flex.AddItem(v, 0, 1, true) + t.app.SetFocus(v) +} + +func (t *App) onRuneL(event *tcell.EventKey) { + title := func() string { + switch { + case !t.viewPath.IsZero() && t.viewNode != "": + return fmt.Sprintf("%s@%s log", t.viewPath, t.viewNode) + case !t.viewPath.IsZero(): + return fmt.Sprintf("%s log", t.viewPath) + case t.viewNode != "": + return fmt.Sprintf("%s log", t.viewNode) + default: + return "" + } + } + + t.initTextView() + t.nav(viewLog) + t.textView.SetDynamicColors(true) + t.textView.SetTitle(title()) + t.textView.SetChangedFunc(func() { + t.textView.ScrollToEnd() + }) + + t.textView.Clear() + + lines := 50 + follow := true + log := t.client.NewGetLogs(t.viewNode). + //SetFilters(nil). + SetLines(&lines). + SetFollow(&follow) + if !t.viewPath.IsZero() { + l := naming.Paths{t.viewPath}.StrSlice() + log.SetPaths(&l) + } + reader, err := log.GetReader() + if err != nil { + t.errorf("%s", err) + return + } + t.logCloser = reader + + w := zerolog.NewConsoleWriter() + w.Out = tview.ANSIWriter(t.textView) + w.TimeFormat = "2006-01-02T15:04:05.000Z07:00" + w.FormatFieldName = func(i any) string { return "" } + w.FormatFieldValue = func(i any) string { return "" } + w.FormatMessage = func(i any) string { + return rawconfig.Colorize.Bold(i) + } + + go func() { + for { + event, err := reader.Read() + if errors.Is(err, io.EOF) { + break + } else if errors.Is(err, context.Canceled) { + break + } else if err != nil { + t.errorf("%s", err) + break + } + rec, err := streamlog.NewEvent(event.Data) + if err != nil { + t.errorf("%s", err) + break + } + switch s := rec.M["JSON"].(type) { + case string: + _, _ = w.Write([]byte(s)) + } + } + }() +} + +func (t *App) onEnter(event *tcell.EventKey) { + switch { + case !t.viewPath.IsZero() && t.viewNode != "": + t.initTextView() + t.nav(viewInstance) + case t.viewPath.Kind == naming.KindCfg || t.viewPath.Kind == naming.KindSec: + t.nav(viewKeys) + } +} + +func (t *App) setFocus() { + switch t.focus() { + case viewConfig: + t.app.SetFocus(t.textView) + case viewInstance: + t.app.SetFocus(t.textView) + case viewLog: + t.app.SetFocus(t.textView) + case viewKeys: + t.app.SetFocus(t.keys) + case viewKey: + t.app.SetFocus(t.textView) + default: + t.app.SetFocus(t.objects) + } +} + +func (t *App) updateKeysView() { + if t.viewPath.IsZero() { + return + } + resp, err := t.client.GetObjectKVStoreKeysWithResponse(context.Background(), t.viewPath.Namespace, t.viewPath.Kind, t.viewPath.Name) + if err != nil { + return + } + if resp.StatusCode() != http.StatusOK { + return + } + t.keys.Clear() + t.keys.SetCell(0, 0, tview.NewTableCell("NAME").SetTextColor(colorTitle).SetSelectable(false)) + t.keys.SetCell(0, 1, tview.NewTableCell("SIZE").SetTextColor(colorTitle).SetSelectable(false)) + for i, key := range resp.JSON200.Items { + row := 1 + i + t.keys.SetCell(row, 0, tview.NewTableCell(key.Key).SetSelectable(true)) + t.keys.SetCell(row, 1, tview.NewTableCell(sizeconv.BSizeCompact(float64(key.Size))).SetSelectable(false)) + } +} + +func (t *App) updateInstanceView() { + digest := t.Frame.Current.GetObjectStatus(t.viewPath) + text := tview.TranslateANSI(digest.Render([]string{t.viewNode})) + t.initTextView() + title := fmt.Sprintf("%s@%s status", t.viewPath, t.viewNode) + t.textView.SetDynamicColors(true) + t.textView.SetTitle(title) + t.textView.Clear() + fmt.Fprint(t.textView, text) +} + +func (t *App) onRuneE(event *tcell.EventKey) { + t.app.Suspend(func() { + if t.viewPath.IsZero() { + return + } + if t.viewKey == "" { + cmd := oxcmd.CmdObjectEditConfig{} + if err := cmd.DoRemote(t.viewPath, t.client); err != nil { + t.errorf("%s", err) + } + } else { + cmd := oxcmd.CmdObjectEditKey{ + Key: t.viewKey, + } + if err := cmd.DoRemote(t.viewPath, t.client); err != nil { + t.errorf("%s", err) + } + } + }) +} + +func (t *App) onRuneC(event *tcell.EventKey) { + t.initTextView() + t.updateConfigView() + t.nav(viewConfig) +} + +func (t *App) updateConfigView() { + if t.viewPath.IsZero() { + return + } + resp, err := t.client.GetObjectConfigFileWithResponse(context.Background(), t.viewPath.Namespace, t.viewPath.Kind, t.viewPath.Name) + if err != nil { + return + } + if resp.StatusCode() != http.StatusOK { + return + } + + text := tview.TranslateANSI(string(resp.Body)) + title := fmt.Sprintf("%s configuration", t.viewPath) + t.textView.SetDynamicColors(false) + t.textView.SetTitle(title) + t.textView.Clear() + fmt.Fprint(t.textView, text) +} + +func (t *App) infof(format string, args ...any) { + t.printf(tcell.ColorGray, format, args...) +} + +func (t *App) warnf(format string, args ...any) { + t.printf(tcell.ColorOrange, format, args...) +} + +func (t *App) errorf(format string, args ...any) { + t.printf(tcell.ColorRed, format, args...) +} + +func (t *App) printf(color tcell.Color, format string, args ...any) { + t.flex.AddItem(t.errs, 1, 0, false) + t.errs.Clear() + t.errs.SetBackgroundColor(color) + fmt.Fprintf(t.errs, format, args...) + time.AfterFunc(5*time.Second, func() { + t.flex.RemoveItem(t.errs) + }) +} + +func (t *App) nav(to viewId) { + from := t.focus() + t.push(to) + if to == from { + return + } + t.navFromTo(from, to) +} + +func (t *App) back() { + from := t.pop() + to := t.focus() + if to == from { + return + } + t.navFromTo(from, to) +} + +func (t *App) navFromTo(from, to viewId) { + t.flex.Clear() + t.flex.AddItem(t.head, 1, 0, false) + switch from { + case viewObject: + case viewLog: + t.textView.SetChangedFunc(nil) + t.textView = nil + if t.logCloser != nil { + t.logCloser.Close() + } + case viewConfig, viewInstance, viewKey: + t.textView = nil + case viewKeys: + t.keys = nil + } + t.updateHead() + switch to { + case viewLog: + t.initTextView() + t.flex.AddItem(t.textView, 0, 1, true) + t.app.SetFocus(t.textView) + case viewConfig: + t.initTextView() + t.flex.AddItem(t.textView, 0, 1, true) + t.app.SetFocus(t.textView) + case viewKey: + t.initTextView() + t.flex.AddItem(t.textView, 0, 1, true) + t.app.SetFocus(t.textView) + case viewInstance: + t.initTextView() + t.flex.AddItem(t.textView, 0, 1, true) + t.app.SetFocus(t.textView) + t.updateInstanceView() + case viewKeys: + t.initKeysTable() + t.flex.AddItem(t.keys, 0, 1, true) + t.app.SetFocus(t.keys) + t.updateKeysView() + case viewObject: + t.flex.AddItem(t.objects, 0, 1, true) + t.app.SetFocus(t.objects) + t.updateObjects() + } + t.flex.AddItem(t.errs, 1, 0, false) +} + +func (t *App) selectedString() string { + if t.viewNode == "" { + if t.viewPath.IsZero() { + return "" + } else { + return t.viewPath.String() + } + } else { + if t.viewPath.IsZero() { + return t.viewNode + } else { + return t.viewPath.String() + "@" + t.viewNode + } + } +} + +func (t *App) initTextView() { + if t.textView != nil { + return + } + v := tview.NewTextView() + v.SetScrollable(true) + v.SetBorder(false) + t.textView = v + return +} + +func (t *App) restart() { + t.restartC <- nil +} + +func (t *App) stop() { + t.exitFlag.Store(true) + t.errC <- nil + t.app.Stop() +} From b86491e358e74423fa35aa75ea6fb43ac9357ebf Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Thu, 24 Oct 2024 12:24:30 +0200 Subject: [PATCH 11/17] Avoid ox tui spurious config and keys requests on every event --- core/tui/main.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/core/tui/main.go b/core/tui/main.go index 5863ec779..dae1e4c5a 100644 --- a/core/tui/main.go +++ b/core/tui/main.go @@ -58,6 +58,8 @@ type ( viewNode string viewKey string + lastUpdatedAt time.Time + firstInstanceCol int firstObjectRow int @@ -195,9 +197,15 @@ func (t *App) resetAllSelected() { } func (t *App) updateKeyTextView() { + if t.viewPath.IsZero() { + return + } if t.viewKey == "" { return } + if t.skipIfConfigNotUpdated() { + return + } resp, err := t.client.GetObjectKVStoreEntryWithResponse(context.Background(), t.viewPath.Namespace, t.viewPath.Kind, t.viewPath.Name, &api.GetObjectKVStoreEntryParams{ Key: t.viewKey, }) @@ -1478,10 +1486,53 @@ func (t *App) setFocus() { } } +func (t *App) getConfigUpdatedAt() time.Time { + path := t.viewPath.String() + for _, nodeData := range t.Current.Cluster.Node { + if instanceData, ok := nodeData.Instance[path]; ok { + return instanceData.Config.UpdatedAt + } + } + return time.Time{} +} + +func (t *App) skipIfConfigNotUpdated() bool { + if updatedAt := t.getConfigUpdatedAt(); updatedAt.IsZero() { + t.errorf("instance config disappeared") + return true + } else if !updatedAt.After(t.lastUpdatedAt) { + return true + } else { + t.lastUpdatedAt = updatedAt + return false + } +} + +func (t *App) skipIfInstanceNotUpdated() bool { + if nodeData, ok := t.Current.Cluster.Node[t.viewNode]; !ok { + t.errorf("node config disappeared") + return true + } else if instanceData, ok := nodeData.Instance[t.viewPath.String()]; !ok { + return true + t.errorf("instance config disappeared") + } else if instanceData.Config.UpdatedAt.After(t.lastUpdatedAt) { + t.lastUpdatedAt = instanceData.Config.UpdatedAt + return false + } else if instanceData.Status.UpdatedAt.After(t.lastUpdatedAt) { + t.lastUpdatedAt = instanceData.Status.UpdatedAt + return false + } + // no change, skip + return true +} + func (t *App) updateKeysView() { if t.viewPath.IsZero() { return } + if t.skipIfConfigNotUpdated() { + return + } resp, err := t.client.GetObjectKVStoreKeysWithResponse(context.Background(), t.viewPath.Namespace, t.viewPath.Kind, t.viewPath.Name) if err != nil { return @@ -1500,6 +1551,15 @@ func (t *App) updateKeysView() { } func (t *App) updateInstanceView() { + if t.viewPath.IsZero() { + return + } + if t.viewNode == "" { + return + } + if t.skipIfInstanceNotUpdated() { + return + } digest := t.Frame.Current.GetObjectStatus(t.viewPath) text := tview.TranslateANSI(digest.Render([]string{t.viewNode})) t.initTextView() @@ -1541,6 +1601,9 @@ func (t *App) updateConfigView() { if t.viewPath.IsZero() { return } + if t.skipIfConfigNotUpdated() { + return + } resp, err := t.client.GetObjectConfigFileWithResponse(context.Background(), t.viewPath.Namespace, t.viewPath.Kind, t.viewPath.Name) if err != nil { return @@ -1600,6 +1663,7 @@ func (t *App) back() { func (t *App) navFromTo(from, to viewId) { t.flex.Clear() t.flex.AddItem(t.head, 1, 0, false) + t.lastUpdatedAt = time.Time{} switch from { case viewObject: case viewLog: From 02462ea47c611ed0ed74a8c2bc5d477ba77ab76c Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Thu, 24 Oct 2024 14:13:50 +0200 Subject: [PATCH 12/17] Add the ox tui instance "do clear" command --- core/tui/main.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/core/tui/main.go b/core/tui/main.go index dae1e4c5a..9a1f5848b 100644 --- a/core/tui/main.go +++ b/core/tui/main.go @@ -885,6 +885,8 @@ func (t *App) onRuneColumn(event *tcell.EventKey) { } instanceAction := func(action string, keys map[[2]string]any) { switch action { + case "clear": + t.actionInstanceClear(keys) case "stop": t.actionInstanceStop(keys) case "start": @@ -1102,6 +1104,19 @@ func (t *App) actionInstanceStart(keys map[[2]string]any) { } } +func (t *App) actionInstanceClear(keys map[[2]string]any) { + ctx := context.Background() + for key, _ := range keys { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + _, _ = t.client.PostInstanceClearWithResponse(ctx, node, p.Namespace, p.Kind, p.Name) + } +} + func (t *App) actionInstanceStop(keys map[[2]string]any) { ctx := context.Background() for key, _ := range keys { @@ -1350,7 +1365,8 @@ func (t *App) onRuneH(event *tcell.EventKey) { unfreeze, unprovision instance actions: - delete, freeze, provision, start, stop, switch, unfreeze, unprovision + clear, delete, freeze, provision, start, stop, switch, unfreeze, + unprovision node actions: drain freeze, unfreeze From dc352a34917e226572ebb2450ebfa394f6006c67 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Thu, 24 Oct 2024 17:30:37 +0200 Subject: [PATCH 13/17] Add ox tui node configuration edit and display Add missing api handlers to do that: * GET /node/name/{nodename}/config/file * PUT /node/name/{nodename}/config/file Add configData to object.Node Support the object.WithConfigData() funcopt on object.Node Modify "ox node edit to use PUT /node/name/{nodename}/config/file --- core/object/factory.go | 9 +- core/object/node.go | 1 + core/object/node_config.go | 13 +- core/oxcmd/lib_remote_config.go | 57 ++- core/oxcmd/node_edit_config.go | 66 ++- core/oxcmd/object_edit_config.go | 4 +- core/tui/main.go | 90 +++- daemon/api/api.yaml | 65 +++ daemon/api/codegen_client_gen.go | 284 +++++++++++++ daemon/api/codegen_server_gen.go | 465 ++++++++++++--------- daemon/daemonapi/get_node_config_file.go | 37 ++ daemon/daemonapi/lib_object_config_file.go | 23 + daemon/daemonapi/put_node_config_file.go | 17 + 13 files changed, 869 insertions(+), 262 deletions(-) create mode 100644 daemon/daemonapi/get_node_config_file.go create mode 100644 daemon/daemonapi/put_node_config_file.go diff --git a/core/object/factory.go b/core/object/factory.go index 696e16a34..261266de6 100644 --- a/core/object/factory.go +++ b/core/object/factory.go @@ -24,8 +24,13 @@ func WithConfigFile(s string) funcopt.O { // Useful for testing volatile services. func WithConfigData(b any) funcopt.O { return funcopt.F(func(t any) error { - o := t.(*core) - o.configData = b + if o, ok := t.(*core); ok { + o.configData = b + } else if o, ok := t.(*Node); ok { + o.configData = b + } else { + return fmt.Errorf("WithConfigData() is not supported on %v", t) + } return nil }) } diff --git a/core/object/node.go b/core/object/node.go index 7440eddd8..812fd49ba 100644 --- a/core/object/node.go +++ b/core/object/node.go @@ -18,6 +18,7 @@ type ( // caches id uuid.UUID + configData any configFile string config *xconfig.T mergedConfig *xconfig.T diff --git a/core/object/node_config.go b/core/object/node_config.go index b20199e38..7e2b8ae72 100644 --- a/core/object/node_config.go +++ b/core/object/node_config.go @@ -31,15 +31,24 @@ func (t *Node) ClusterConfigFile() string { } func (t *Node) loadConfig() error { + var sources []any nodeConfigFile := t.ConfigFile() - if config, err := xconfig.NewObject(nodeConfigFile, nodeConfigFile); err != nil { + + if t.configData != nil { + sources = []any{t.configData} + } else { + sources = []any{nodeConfigFile} + } + + if config, err := xconfig.NewObject(nodeConfigFile, sources...); err != nil { return err } else { t.config = config t.config.Referrer = t } clusterConfigFile := t.ClusterConfigFile() - if config, err := xconfig.NewObject(clusterConfigFile, clusterConfigFile, nodeConfigFile); err != nil { + sources = append([]any{clusterConfigFile}, sources...) + if config, err := xconfig.NewObject(clusterConfigFile, sources...); err != nil { return err } else { t.mergedConfig = config diff --git a/core/oxcmd/lib_remote_config.go b/core/oxcmd/lib_remote_config.go index 0a7befc3f..f2a77879a 100644 --- a/core/oxcmd/lib_remote_config.go +++ b/core/oxcmd/lib_remote_config.go @@ -10,16 +10,25 @@ import ( "github.com/opensvc/om3/core/naming" ) -func createTempRemoteConfig(p naming.Path, c *client.T) (string, error) { - var ( - err error - buff []byte - f *os.File - ) - if buff, err = fetchConfig(p, c); err != nil { +func createTempRemoteNodeConfig(nodename string, c *client.T) (string, error) { + if buff, err := fetchNodeConfig(nodename, c); err != nil { return "", err + } else { + return createTempRemoteConfig(buff) } - if f, err = os.CreateTemp("", ".opensvc.remote.config.*"); err != nil { +} + +func createTempRemoteObjectConfig(p naming.Path, c *client.T) (string, error) { + if buff, err := fetchObjectConfig(p, c); err != nil { + return "", err + } else { + return createTempRemoteConfig(buff) + } +} + +func createTempRemoteConfig(buff []byte) (string, error) { + f, err := os.CreateTemp("", ".opensvc.remote.config.*") + if err != nil { return "", err } filename := f.Name() @@ -30,7 +39,17 @@ func createTempRemoteConfig(p naming.Path, c *client.T) (string, error) { return filename, nil } -func fetchConfig(p naming.Path, c *client.T) ([]byte, error) { +func fetchNodeConfig(nodename string, c *client.T) ([]byte, error) { + resp, err := c.GetNodeConfigFileWithResponse(context.Background(), nodename) + if err != nil { + return nil, err + } else if resp.StatusCode() != http.StatusOK { + return nil, fmt.Errorf("get node %s file from %s: %s", nodename, c.URL(), resp.Status()) + } + return resp.Body, nil +} + +func fetchObjectConfig(p naming.Path, c *client.T) ([]byte, error) { resp, err := c.GetObjectConfigFileWithResponse(context.Background(), p.Namespace, p.Kind, p.Name) if err != nil { return nil, err @@ -40,7 +59,7 @@ func fetchConfig(p naming.Path, c *client.T) ([]byte, error) { return resp.Body, nil } -func putConfig(p naming.Path, fName string, c *client.T) (err error) { +func putObjectConfig(p naming.Path, fName string, c *client.T) (err error) { file, err := os.Open(fName) if err != nil { return err @@ -57,3 +76,21 @@ func putConfig(p naming.Path, fName string, c *client.T) (err error) { return fmt.Errorf("put object %s file from %s: %s", p, c.URL(), resp.Status()+string(resp.Body)) } } + +func putNodeConfig(nodename, fName string, c *client.T) (err error) { + file, err := os.Open(fName) + if err != nil { + return err + } + defer file.Close() + resp, err := c.PutNodeConfigFileWithBodyWithResponse(context.Background(), nodename, "application/octet-stream", file) + if err != nil { + return err + } + switch resp.StatusCode() { + case http.StatusNoContent: + return nil + default: + return fmt.Errorf("put node %s file from %s: %s", nodename, c.URL(), resp.Status()+string(resp.Body)) + } +} diff --git a/core/oxcmd/node_edit_config.go b/core/oxcmd/node_edit_config.go index 9eff05c69..de7052fd0 100644 --- a/core/oxcmd/node_edit_config.go +++ b/core/oxcmd/node_edit_config.go @@ -1,31 +1,65 @@ package oxcmd import ( - "github.com/opensvc/om3/core/object" + "fmt" + "os" + + "github.com/opensvc/om3/core/client" + "github.com/opensvc/om3/core/clientcontext" + "github.com/opensvc/om3/util/editor" + "github.com/opensvc/om3/util/file" + "github.com/opensvc/om3/util/hostname" ) type ( CmdNodeEditConfig struct { OptsGlobal - Discard bool - Recover bool + NodeSelector string + Discard bool + Recover bool } ) +func (t *CmdNodeEditConfig) DoRemote(nodename string, c *client.T) error { + var ( + err error + refSum []byte + filename string + ) + if filename, err = createTempRemoteNodeConfig(nodename, c); err != nil { + return err + } + defer os.Remove(filename) + if refSum, err = file.MD5(filename); err != nil { + return err + } + if err = editor.Edit(filename); err != nil { + return err + } + if file.HaveSameMD5(refSum, filename) { + fmt.Println("unchanged") + return nil + } + if err = putNodeConfig(nodename, filename, c); err != nil { + return err + } + return nil +} + func (t *CmdNodeEditConfig) Run() error { - n, err := object.NewNode() - if err != nil { + nodename := t.NodeSelector + if !clientcontext.IsSet() && nodename == "" { + nodename = hostname.Hostname() + } + if nodename == "" { + return fmt.Errorf("--node must be specified") + } + var ( + c *client.T + err error + ) + if c, err = client.New(client.WithURL(t.Server)); err != nil { return err } - switch { - //case t.Discard && t.Recover: - // return fmt.Errorf("discard and recover options are mutually exclusive") - case t.Discard: - err = n.DiscardAndEditConfig() - case t.Recover: - err = n.RecoverAndEditConfig() - default: - err = n.EditConfig() - } - return err + return t.DoRemote(nodename, c) } diff --git a/core/oxcmd/object_edit_config.go b/core/oxcmd/object_edit_config.go index bea05dfc9..8bc40c606 100644 --- a/core/oxcmd/object_edit_config.go +++ b/core/oxcmd/object_edit_config.go @@ -42,7 +42,7 @@ func (t *CmdObjectEditConfig) DoRemote(p naming.Path, c *client.T) error { refSum []byte filename string ) - if filename, err = createTempRemoteConfig(p, c); err != nil { + if filename, err = createTempRemoteObjectConfig(p, c); err != nil { return err } defer os.Remove(filename) @@ -56,7 +56,7 @@ func (t *CmdObjectEditConfig) DoRemote(p naming.Path, c *client.T) error { fmt.Println("unchanged") return nil } - if err = putConfig(p, filename, c); err != nil { + if err = putObjectConfig(p, filename, c); err != nil { return err } return nil diff --git a/core/tui/main.go b/core/tui/main.go index 9a1f5848b..7b9b6ba23 100644 --- a/core/tui/main.go +++ b/core/tui/main.go @@ -379,18 +379,6 @@ func (t *App) initApp() { case tcell.KeyESC: t.back() case tcell.KeyTab: - v := t.focus() - v++ - if v >= viewLast { - v = 0 - } - if primitive := t.viewPrimitive(v); primitive != nil { - if primitive == nil { - t.infof("xx no primitive for %d", v) - } - //t.app.SetFocus(primitive) - } - return nil } if t.command != nil { return event @@ -1524,6 +1512,21 @@ func (t *App) skipIfConfigNotUpdated() bool { } } +func (t *App) skipIfNodeConfigNotUpdated() bool { + return false // TODO: Add a UpdatedAt field in node.Config + /* + if nodeData, ok := t.Current.Cluster.Node[t.viewNode]; !ok { + t.errorf("node config disappeared") + return true + } else if nodeData.Config.UpdatedAt.After(t.lastUpdatedAt) { + t.lastUpdatedAt = instanceData.Config.UpdatedAt + return false + } + // no change, skip + return true + */ +} + func (t *App) skipIfInstanceNotUpdated() bool { if nodeData, ok := t.Current.Cluster.Node[t.viewNode]; !ok { t.errorf("node config disappeared") @@ -1588,21 +1591,32 @@ func (t *App) updateInstanceView() { func (t *App) onRuneE(event *tcell.EventKey) { t.app.Suspend(func() { - if t.viewPath.IsZero() { - return - } - if t.viewKey == "" { - cmd := oxcmd.CmdObjectEditConfig{} - if err := cmd.DoRemote(t.viewPath, t.client); err != nil { - t.errorf("%s", err) - } - } else { + row, col := t.objects.GetSelection() + switch { + case !t.viewPath.IsZero() && t.viewKey != "": cmd := oxcmd.CmdObjectEditKey{ Key: t.viewKey, } if err := cmd.DoRemote(t.viewPath, t.client); err != nil { t.errorf("%s", err) } + case !t.viewPath.IsZero(): + cmd := oxcmd.CmdObjectEditConfig{} + if err := cmd.DoRemote(t.viewPath, t.client); err != nil { + t.errorf("%s", err) + } + case t.viewNode != "": + cmd := oxcmd.CmdNodeEditConfig{} + if err := cmd.DoRemote(t.viewNode, t.client); err != nil { + t.errorf("%s", err) + } + case row == 0 && col == 1: + /* + cmd := oxcmd.CmdClusterEditConfig{} + if err := cmd.DoRemote(t.viewPath, t.client); err != nil { + t.errorf("%s", err) + } + */ } }) } @@ -1614,9 +1628,41 @@ func (t *App) onRuneC(event *tcell.EventKey) { } func (t *App) updateConfigView() { - if t.viewPath.IsZero() { + row, col := t.objects.GetSelection() + switch { + case !t.viewPath.IsZero(): + t.updateObjectConfigView() + case t.viewNode != "": + t.updateNodeConfigView() + case row == 0 && col == 1: + t.updateClusterConfigView() + } +} + +func (t *App) updateClusterConfigView() { +} + +func (t *App) updateNodeConfigView() { + if t.skipIfNodeConfigNotUpdated() { + return + } + resp, err := t.client.GetNodeConfigFileWithResponse(context.Background(), t.viewNode) + if err != nil { return } + if resp.StatusCode() != http.StatusOK { + return + } + + text := tview.TranslateANSI(string(resp.Body)) + title := fmt.Sprintf("%s configuration", t.viewPath) + t.textView.SetDynamicColors(false) + t.textView.SetTitle(title) + t.textView.Clear() + fmt.Fprint(t.textView, text) +} + +func (t *App) updateObjectConfigView() { if t.skipIfConfigNotUpdated() { return } diff --git a/daemon/api/api.yaml b/daemon/api/api.yaml index d33d5def5..0094845ee 100644 --- a/daemon/api/api.yaml +++ b/daemon/api/api.yaml @@ -917,6 +917,71 @@ paths: 500: $ref: '#/components/responses/500' + /node/name/{nodename}/config/file: + get: + description: | + Return the node configuration. If nodename is not the local node name, do proxy. + operationId: GetNodeConfigFile + tags: + - node + security: + - basicAuth: [] + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/inPathNodeName' + responses: + 200: + description: OK + headers: + x-last-modified-rfc3339nano: + type: string + format: date-time + pattern: '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?Z?$' + content: + application/octet-stream: + schema: + type: string + format: binary + 400: + $ref: '#/components/responses/400' + 401: + $ref: '#/components/responses/401' + 403: + $ref: '#/components/responses/403' + 500: + $ref: '#/components/responses/500' + put: + operationId: PutNodeConfigFile + tags: + - node + security: + - basicAuth: [] + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/inPathNodeName' + requestBody: + description: OK + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + 204: + $ref: '#/components/responses/204' + 400: + $ref: '#/components/responses/400' + 401: + $ref: '#/components/responses/401' + 403: + $ref: '#/components/responses/403' + 404: + $ref: '#/components/responses/404' + 409: + $ref: '#/components/responses/409' + 500: + $ref: '#/components/responses/500' + /node/name/{nodename}/config/get: get: operationId: GetNodeConfigGet diff --git a/daemon/api/codegen_client_gen.go b/daemon/api/codegen_client_gen.go index ce244486d..27b912404 100644 --- a/daemon/api/codegen_client_gen.go +++ b/daemon/api/codegen_client_gen.go @@ -190,6 +190,12 @@ type ClientInterface interface { // GetNodeConfig request GetNodeConfig(ctx context.Context, nodename InPathNodeName, params *GetNodeConfigParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetNodeConfigFile request + GetNodeConfigFile(ctx context.Context, nodename InPathNodeName, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PutNodeConfigFileWithBody request with any body + PutNodeConfigFileWithBody(ctx context.Context, nodename InPathNodeName, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetNodeConfigGet request GetNodeConfigGet(ctx context.Context, nodename InPathNodeName, params *GetNodeConfigGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -873,6 +879,30 @@ func (c *Client) GetNodeConfig(ctx context.Context, nodename InPathNodeName, par return c.Client.Do(req) } +func (c *Client) GetNodeConfigFile(ctx context.Context, nodename InPathNodeName, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetNodeConfigFileRequest(c.Server, nodename) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PutNodeConfigFileWithBody(ctx context.Context, nodename InPathNodeName, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPutNodeConfigFileRequestWithBody(c.Server, nodename, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) GetNodeConfigGet(ctx context.Context, nodename InPathNodeName, params *GetNodeConfigGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewGetNodeConfigGetRequest(c.Server, nodename, params) if err != nil { @@ -3402,6 +3432,76 @@ func NewGetNodeConfigRequest(server string, nodename InPathNodeName, params *Get return req, nil } +// NewGetNodeConfigFileRequest generates requests for GetNodeConfigFile +func NewGetNodeConfigFileRequest(server string, nodename InPathNodeName) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "nodename", runtime.ParamLocationPath, nodename) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/node/name/%s/config/file", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPutNodeConfigFileRequestWithBody generates requests for PutNodeConfigFile with any type of body +func NewPutNodeConfigFileRequestWithBody(server string, nodename InPathNodeName, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "nodename", runtime.ParamLocationPath, nodename) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/node/name/%s/config/file", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewGetNodeConfigGetRequest generates requests for GetNodeConfigGet func NewGetNodeConfigGetRequest(server string, nodename InPathNodeName, params *GetNodeConfigGetParams) (*http.Request, error) { var err error @@ -9404,6 +9504,12 @@ type ClientWithResponsesInterface interface { // GetNodeConfigWithResponse request GetNodeConfigWithResponse(ctx context.Context, nodename InPathNodeName, params *GetNodeConfigParams, reqEditors ...RequestEditorFn) (*GetNodeConfigResponse, error) + // GetNodeConfigFileWithResponse request + GetNodeConfigFileWithResponse(ctx context.Context, nodename InPathNodeName, reqEditors ...RequestEditorFn) (*GetNodeConfigFileResponse, error) + + // PutNodeConfigFileWithBodyWithResponse request with any body + PutNodeConfigFileWithBodyWithResponse(ctx context.Context, nodename InPathNodeName, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PutNodeConfigFileResponse, error) + // GetNodeConfigGetWithResponse request GetNodeConfigGetWithResponse(ctx context.Context, nodename InPathNodeName, params *GetNodeConfigGetParams, reqEditors ...RequestEditorFn) (*GetNodeConfigGetResponse, error) @@ -10464,6 +10570,58 @@ func (r GetNodeConfigResponse) StatusCode() int { return 0 } +type GetNodeConfigFileResponse struct { + Body []byte + HTTPResponse *http.Response + JSON400 *N400 + JSON401 *N401 + JSON403 *N403 + JSON500 *N500 +} + +// Status returns HTTPResponse.Status +func (r GetNodeConfigFileResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetNodeConfigFileResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PutNodeConfigFileResponse struct { + Body []byte + HTTPResponse *http.Response + JSON400 *N400 + JSON401 *N401 + JSON403 *N403 + JSON404 *N404 + JSON409 *N409 + JSON500 *N500 +} + +// Status returns HTTPResponse.Status +func (r PutNodeConfigFileResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PutNodeConfigFileResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type GetNodeConfigGetResponse struct { Body []byte HTTPResponse *http.Response @@ -12974,6 +13132,24 @@ func (c *ClientWithResponses) GetNodeConfigWithResponse(ctx context.Context, nod return ParseGetNodeConfigResponse(rsp) } +// GetNodeConfigFileWithResponse request returning *GetNodeConfigFileResponse +func (c *ClientWithResponses) GetNodeConfigFileWithResponse(ctx context.Context, nodename InPathNodeName, reqEditors ...RequestEditorFn) (*GetNodeConfigFileResponse, error) { + rsp, err := c.GetNodeConfigFile(ctx, nodename, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetNodeConfigFileResponse(rsp) +} + +// PutNodeConfigFileWithBodyWithResponse request with arbitrary body returning *PutNodeConfigFileResponse +func (c *ClientWithResponses) PutNodeConfigFileWithBodyWithResponse(ctx context.Context, nodename InPathNodeName, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PutNodeConfigFileResponse, error) { + rsp, err := c.PutNodeConfigFileWithBody(ctx, nodename, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePutNodeConfigFileResponse(rsp) +} + // GetNodeConfigGetWithResponse request returning *GetNodeConfigGetResponse func (c *ClientWithResponses) GetNodeConfigGetWithResponse(ctx context.Context, nodename InPathNodeName, params *GetNodeConfigGetParams, reqEditors ...RequestEditorFn) (*GetNodeConfigGetResponse, error) { rsp, err := c.GetNodeConfigGet(ctx, nodename, params, reqEditors...) @@ -15381,6 +15557,114 @@ func ParseGetNodeConfigResponse(rsp *http.Response) (*GetNodeConfigResponse, err return response, nil } +// ParseGetNodeConfigFileResponse parses an HTTP response from a GetNodeConfigFileWithResponse call +func ParseGetNodeConfigFileResponse(rsp *http.Response) (*GetNodeConfigFileResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetNodeConfigFileResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest N400 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest N401 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest N403 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest N500 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParsePutNodeConfigFileResponse parses an HTTP response from a PutNodeConfigFileWithResponse call +func ParsePutNodeConfigFileResponse(rsp *http.Response) (*PutNodeConfigFileResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PutNodeConfigFileResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest N400 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest N401 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest N403 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest N404 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest N409 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest N500 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseGetNodeConfigGetResponse parses an HTTP response from a GetNodeConfigGetWithResponse call func ParseGetNodeConfigGetResponse(rsp *http.Response) (*GetNodeConfigGetResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/daemon/api/codegen_server_gen.go b/daemon/api/codegen_server_gen.go index 5e893fb6d..ded9a6e3c 100644 --- a/daemon/api/codegen_server_gen.go +++ b/daemon/api/codegen_server_gen.go @@ -114,6 +114,12 @@ type ServerInterface interface { // (GET /node/name/{nodename}/config) GetNodeConfig(ctx echo.Context, nodename InPathNodeName, params GetNodeConfigParams) error + // (GET /node/name/{nodename}/config/file) + GetNodeConfigFile(ctx echo.Context, nodename InPathNodeName) error + + // (PUT /node/name/{nodename}/config/file) + PutNodeConfigFile(ctx echo.Context, nodename InPathNodeName) error + // (GET /node/name/{nodename}/config/get) GetNodeConfigGet(ctx echo.Context, nodename InPathNodeName, params GetNodeConfigGetParams) error @@ -1089,6 +1095,46 @@ func (w *ServerInterfaceWrapper) GetNodeConfig(ctx echo.Context) error { return err } +// GetNodeConfigFile converts echo context to params. +func (w *ServerInterfaceWrapper) GetNodeConfigFile(ctx echo.Context) error { + var err error + // ------------- Path parameter "nodename" ------------- + var nodename InPathNodeName + + err = runtime.BindStyledParameterWithLocation("simple", false, "nodename", runtime.ParamLocationPath, ctx.Param("nodename"), &nodename) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter nodename: %s", err)) + } + + ctx.Set(BasicAuthScopes, []string{}) + + ctx.Set(BearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetNodeConfigFile(ctx, nodename) + return err +} + +// PutNodeConfigFile converts echo context to params. +func (w *ServerInterfaceWrapper) PutNodeConfigFile(ctx echo.Context) error { + var err error + // ------------- Path parameter "nodename" ------------- + var nodename InPathNodeName + + err = runtime.BindStyledParameterWithLocation("simple", false, "nodename", runtime.ParamLocationPath, ctx.Param("nodename"), &nodename) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter nodename: %s", err)) + } + + ctx.Set(BasicAuthScopes, []string{}) + + ctx.Set(BearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.PutNodeConfigFile(ctx, nodename) + return err +} + // GetNodeConfigGet converts echo context to params. func (w *ServerInterfaceWrapper) GetNodeConfigGet(ctx echo.Context) error { var err error @@ -4654,6 +4700,8 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.POST(baseURL+"/node/name/:nodename/action/unfreeze", wrapper.PostPeerActionUnfreeze) router.GET(baseURL+"/node/name/:nodename/capabilities", wrapper.GetNodeCapabilities) router.GET(baseURL+"/node/name/:nodename/config", wrapper.GetNodeConfig) + router.GET(baseURL+"/node/name/:nodename/config/file", wrapper.GetNodeConfigFile) + router.PUT(baseURL+"/node/name/:nodename/config/file", wrapper.PutNodeConfigFile) router.GET(baseURL+"/node/name/:nodename/config/get", wrapper.GetNodeConfigGet) router.POST(baseURL+"/node/name/:nodename/config/update", wrapper.PostNodeConfigUpdate) router.POST(baseURL+"/node/name/:nodename/daemon/action/restart", wrapper.PostDaemonRestart) @@ -4744,214 +4792,215 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "AP//8mMBLHv1AQA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/daemon/daemonapi/get_node_config_file.go b/daemon/daemonapi/get_node_config_file.go new file mode 100644 index 000000000..161ed9ce3 --- /dev/null +++ b/daemon/daemonapi/get_node_config_file.go @@ -0,0 +1,37 @@ +package daemonapi + +import ( + "net/http" + "time" + + "github.com/labstack/echo/v4" + + "github.com/opensvc/om3/core/client" + "github.com/opensvc/om3/core/rawconfig" + "github.com/opensvc/om3/daemon/api" + "github.com/opensvc/om3/daemon/rbac" + "github.com/opensvc/om3/util/file" +) + +func (a *DaemonAPI) GetNodeConfigFile(ctx echo.Context, nodename string) error { + if v, err := assertRole(ctx, rbac.RoleRoot); !v { + return err + } + if a.localhost == nodename { + logName := "GetNodeConfigFile" + log := LogHandler(ctx, logName) + log.Debugf("%s: starting", logName) + + filename := rawconfig.NodeConfigFile() + mtime := file.ModTime(filename) + if !mtime.IsZero() { + ctx.Response().Header().Add(api.HeaderLastModifiedNano, mtime.Format(time.RFC3339Nano)) + log.Infof("serve node config file to %s", userFromContext(ctx).GetUserName()) + return ctx.File(filename) + } + return JSONProblemf(ctx, http.StatusNotFound, "Not found", "Config file not found for %s@%s", a.localhost) + } + return a.proxy(ctx, nodename, func(c *client.T) (*http.Response, error) { + return c.GetNodeConfigFile(ctx.Request().Context(), nodename) + }) +} diff --git a/daemon/daemonapi/lib_object_config_file.go b/daemon/daemonapi/lib_object_config_file.go index 3809890a8..77652fee6 100644 --- a/daemon/daemonapi/lib_object_config_file.go +++ b/daemon/daemonapi/lib_object_config_file.go @@ -33,3 +33,26 @@ func (a *DaemonAPI) writeObjectConfigFile(ctx echo.Context, p naming.Path) error } return ctx.NoContent(http.StatusNoContent) } + +func (a *DaemonAPI) writeNodeConfigFile(ctx echo.Context, nodename string) error { + body, err := io.ReadAll(ctx.Request().Body) + if err != nil { + return JSONProblemf(ctx, http.StatusInternalServerError, "Read body", "%s", err) + } + o, err := object.NewNode(object.WithConfigData(body)) + if err != nil { + return JSONProblemf(ctx, http.StatusInternalServerError, "New object", "%s", err) + } + alerts, err := o.ValidateConfig() + if err != nil { + return JSONProblemf(ctx, http.StatusInternalServerError, "Validate config", "%s", err) + } + if alerts.HasError() { + return JSONProblemf(ctx, http.StatusBadRequest, "Validate config", "%s", err) + } + // Use the non-validating commit func as we already validate to emit a explicit error + if err := o.Config().RecommitInvalid(); err != nil { + return JSONProblemf(ctx, http.StatusInternalServerError, "Commit", "%s", err) + } + return ctx.NoContent(http.StatusNoContent) +} diff --git a/daemon/daemonapi/put_node_config_file.go b/daemon/daemonapi/put_node_config_file.go new file mode 100644 index 000000000..0177b0338 --- /dev/null +++ b/daemon/daemonapi/put_node_config_file.go @@ -0,0 +1,17 @@ +package daemonapi + +import ( + "net/http" + + "github.com/labstack/echo/v4" + "github.com/opensvc/om3/core/client" +) + +func (a *DaemonAPI) PutNodeConfigFile(ctx echo.Context, nodename string) error { + if nodename == a.localhost { + return a.writeNodeConfigFile(ctx, nodename) + } + return a.proxy(ctx, nodename, func(c *client.T) (*http.Response, error) { + return c.PutNodeConfigFileWithBody(ctx.Request().Context(), nodename, "application/octet-stream", ctx.Request().Body) + }) +} From 029b03de599be1bd969b43ac808d660c7b47d32a Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Thu, 24 Oct 2024 18:27:24 +0200 Subject: [PATCH 14/17] Add ox tui cluster configuration edit and display Add missing api handlers to do that: * GET /cluster/config/file * PUT /cluster/config/file --- core/tui/main.go | 46 +- core/xconfig/edit.go | 3 +- daemon/api/api.yaml | 61 +++ daemon/api/codegen_client_gen.go | 270 ++++++++++++ daemon/api/codegen_server_gen.go | 444 +++++++++++--------- daemon/daemonapi/get_cluster_config_file.go | 32 ++ daemon/daemonapi/put_cluster_config_file.go | 11 + 7 files changed, 638 insertions(+), 229 deletions(-) create mode 100644 daemon/daemonapi/get_cluster_config_file.go create mode 100644 daemon/daemonapi/put_cluster_config_file.go diff --git a/core/tui/main.go b/core/tui/main.go index 7b9b6ba23..c60a7ea04 100644 --- a/core/tui/main.go +++ b/core/tui/main.go @@ -1512,21 +1512,6 @@ func (t *App) skipIfConfigNotUpdated() bool { } } -func (t *App) skipIfNodeConfigNotUpdated() bool { - return false // TODO: Add a UpdatedAt field in node.Config - /* - if nodeData, ok := t.Current.Cluster.Node[t.viewNode]; !ok { - t.errorf("node config disappeared") - return true - } else if nodeData.Config.UpdatedAt.After(t.lastUpdatedAt) { - t.lastUpdatedAt = instanceData.Config.UpdatedAt - return false - } - // no change, skip - return true - */ -} - func (t *App) skipIfInstanceNotUpdated() bool { if nodeData, ok := t.Current.Cluster.Node[t.viewNode]; !ok { t.errorf("node config disappeared") @@ -1611,12 +1596,10 @@ func (t *App) onRuneE(event *tcell.EventKey) { t.errorf("%s", err) } case row == 0 && col == 1: - /* - cmd := oxcmd.CmdClusterEditConfig{} - if err := cmd.DoRemote(t.viewPath, t.client); err != nil { - t.errorf("%s", err) - } - */ + cmd := oxcmd.CmdObjectEditConfig{} + if err := cmd.DoRemote(naming.Cluster, t.client); err != nil { + t.errorf("%s", err) + } } }) } @@ -1640,12 +1623,31 @@ func (t *App) updateConfigView() { } func (t *App) updateClusterConfigView() { + if !t.lastUpdatedAt.IsZero() { + return + } + t.lastUpdatedAt = time.Now() + resp, err := t.client.GetClusterConfigFileWithResponse(context.Background()) + if err != nil { + return + } + if resp.StatusCode() != http.StatusOK { + return + } + + text := tview.TranslateANSI(string(resp.Body)) + title := fmt.Sprintf("cluster configuration") + t.textView.SetDynamicColors(false) + t.textView.SetTitle(title) + t.textView.Clear() + fmt.Fprint(t.textView, text) } func (t *App) updateNodeConfigView() { - if t.skipIfNodeConfigNotUpdated() { + if !t.lastUpdatedAt.IsZero() { return } + t.lastUpdatedAt = time.Now() resp, err := t.client.GetNodeConfigFileWithResponse(context.Background(), t.viewNode) if err != nil { return diff --git a/core/xconfig/edit.go b/core/xconfig/edit.go index 469aa6f63..eaf7391c1 100644 --- a/core/xconfig/edit.go +++ b/core/xconfig/edit.go @@ -8,7 +8,6 @@ import ( "github.com/hexops/gotextdiff" "github.com/hexops/gotextdiff/myers" "github.com/hexops/gotextdiff/span" - "github.com/rs/zerolog/log" "github.com/opensvc/om3/util/editor" "github.com/opensvc/om3/util/file" @@ -62,7 +61,7 @@ func Edit(src string, mode EditMode, ref Referrer) error { if err := file.Copy(src, dst); err != nil { return err } - log.Debug().Str("dst", dst).Msg("new configuration temporary copy") + //log.Debug().Str("dst", dst).Msg("new configuration temporary copy") } var refSum []byte if b, err := file.MD5(dst); err != nil { diff --git a/daemon/api/api.yaml b/daemon/api/api.yaml index 0094845ee..4787e0917 100644 --- a/daemon/api/api.yaml +++ b/daemon/api/api.yaml @@ -161,6 +161,67 @@ paths: tags: - cluster + /cluster/config/file: + get: + description: | + Return the cluster configuration. + operationId: GetClusterConfigFile + tags: + - cluster + security: + - basicAuth: [] + - bearerAuth: [] + responses: + 200: + description: OK + headers: + x-last-modified-rfc3339nano: + type: string + format: date-time + pattern: '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?Z?$' + content: + application/octet-stream: + schema: + type: string + format: binary + 400: + $ref: '#/components/responses/400' + 401: + $ref: '#/components/responses/401' + 403: + $ref: '#/components/responses/403' + 500: + $ref: '#/components/responses/500' + put: + operationId: PutClusterConfigFile + tags: + - cluster + security: + - basicAuth: [] + - bearerAuth: [] + requestBody: + description: OK + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + 204: + $ref: '#/components/responses/204' + 400: + $ref: '#/components/responses/400' + 401: + $ref: '#/components/responses/401' + 403: + $ref: '#/components/responses/403' + 404: + $ref: '#/components/responses/404' + 409: + $ref: '#/components/responses/409' + 500: + $ref: '#/components/responses/500' + /daemon/action/join: post: description: | diff --git a/daemon/api/codegen_client_gen.go b/daemon/api/codegen_client_gen.go index 27b912404..299e148af 100644 --- a/daemon/api/codegen_client_gen.go +++ b/daemon/api/codegen_client_gen.go @@ -104,6 +104,12 @@ type ClientInterface interface { // PostClusterActionUnfreeze request PostClusterActionUnfreeze(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetClusterConfigFile request + GetClusterConfigFile(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PutClusterConfigFileWithBody request with any body + PutClusterConfigFileWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + // PostDaemonJoin request PostDaemonJoin(ctx context.Context, params *PostDaemonJoinParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -519,6 +525,30 @@ func (c *Client) PostClusterActionUnfreeze(ctx context.Context, reqEditors ...Re return c.Client.Do(req) } +func (c *Client) GetClusterConfigFile(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetClusterConfigFileRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PutClusterConfigFileWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPutClusterConfigFileRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) PostDaemonJoin(ctx context.Context, params *PostDaemonJoinParams, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewPostDaemonJoinRequest(c.Server, params) if err != nil { @@ -2144,6 +2174,62 @@ func NewPostClusterActionUnfreezeRequest(server string) (*http.Request, error) { return req, nil } +// NewGetClusterConfigFileRequest generates requests for GetClusterConfigFile +func NewGetClusterConfigFileRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/cluster/config/file") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPutClusterConfigFileRequestWithBody generates requests for PutClusterConfigFile with any type of body +func NewPutClusterConfigFileRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/cluster/config/file") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewPostDaemonJoinRequest generates requests for PostDaemonJoin func NewPostDaemonJoinRequest(server string, params *PostDaemonJoinParams) (*http.Request, error) { var err error @@ -9418,6 +9504,12 @@ type ClientWithResponsesInterface interface { // PostClusterActionUnfreezeWithResponse request PostClusterActionUnfreezeWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostClusterActionUnfreezeResponse, error) + // GetClusterConfigFileWithResponse request + GetClusterConfigFileWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetClusterConfigFileResponse, error) + + // PutClusterConfigFileWithBodyWithResponse request with any body + PutClusterConfigFileWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PutClusterConfigFileResponse, error) + // PostDaemonJoinWithResponse request PostDaemonJoinWithResponse(ctx context.Context, params *PostDaemonJoinParams, reqEditors ...RequestEditorFn) (*PostDaemonJoinResponse, error) @@ -9907,6 +9999,58 @@ func (r PostClusterActionUnfreezeResponse) StatusCode() int { return 0 } +type GetClusterConfigFileResponse struct { + Body []byte + HTTPResponse *http.Response + JSON400 *N400 + JSON401 *N401 + JSON403 *N403 + JSON500 *N500 +} + +// Status returns HTTPResponse.Status +func (r GetClusterConfigFileResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetClusterConfigFileResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PutClusterConfigFileResponse struct { + Body []byte + HTTPResponse *http.Response + JSON400 *N400 + JSON401 *N401 + JSON403 *N403 + JSON404 *N404 + JSON409 *N409 + JSON500 *N500 +} + +// Status returns HTTPResponse.Status +func (r PutClusterConfigFileResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PutClusterConfigFileResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type PostDaemonJoinResponse struct { Body []byte HTTPResponse *http.Response @@ -12866,6 +13010,24 @@ func (c *ClientWithResponses) PostClusterActionUnfreezeWithResponse(ctx context. return ParsePostClusterActionUnfreezeResponse(rsp) } +// GetClusterConfigFileWithResponse request returning *GetClusterConfigFileResponse +func (c *ClientWithResponses) GetClusterConfigFileWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetClusterConfigFileResponse, error) { + rsp, err := c.GetClusterConfigFile(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetClusterConfigFileResponse(rsp) +} + +// PutClusterConfigFileWithBodyWithResponse request with arbitrary body returning *PutClusterConfigFileResponse +func (c *ClientWithResponses) PutClusterConfigFileWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PutClusterConfigFileResponse, error) { + rsp, err := c.PutClusterConfigFileWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePutClusterConfigFileResponse(rsp) +} + // PostDaemonJoinWithResponse request returning *PostDaemonJoinResponse func (c *ClientWithResponses) PostDaemonJoinWithResponse(ctx context.Context, params *PostDaemonJoinParams, reqEditors ...RequestEditorFn) (*PostDaemonJoinResponse, error) { rsp, err := c.PostDaemonJoin(ctx, params, reqEditors...) @@ -14244,6 +14406,114 @@ func ParsePostClusterActionUnfreezeResponse(rsp *http.Response) (*PostClusterAct return response, nil } +// ParseGetClusterConfigFileResponse parses an HTTP response from a GetClusterConfigFileWithResponse call +func ParseGetClusterConfigFileResponse(rsp *http.Response) (*GetClusterConfigFileResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetClusterConfigFileResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest N400 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest N401 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest N403 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest N500 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParsePutClusterConfigFileResponse parses an HTTP response from a PutClusterConfigFileWithResponse call +func ParsePutClusterConfigFileResponse(rsp *http.Response) (*PutClusterConfigFileResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PutClusterConfigFileResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest N400 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest N401 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest N403 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest N404 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest N409 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest N500 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParsePostDaemonJoinResponse parses an HTTP response from a PostDaemonJoinWithResponse call func ParsePostDaemonJoinResponse(rsp *http.Response) (*PostDaemonJoinResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/daemon/api/codegen_server_gen.go b/daemon/api/codegen_server_gen.go index ded9a6e3c..ae9500a36 100644 --- a/daemon/api/codegen_server_gen.go +++ b/daemon/api/codegen_server_gen.go @@ -36,6 +36,12 @@ type ServerInterface interface { // (POST /cluster/action/unfreeze) PostClusterActionUnfreeze(ctx echo.Context) error + // (GET /cluster/config/file) + GetClusterConfigFile(ctx echo.Context) error + + // (PUT /cluster/config/file) + PutClusterConfigFile(ctx echo.Context) error + // (POST /daemon/action/join) PostDaemonJoin(ctx echo.Context, params PostDaemonJoinParams) error @@ -455,6 +461,32 @@ func (w *ServerInterfaceWrapper) PostClusterActionUnfreeze(ctx echo.Context) err return err } +// GetClusterConfigFile converts echo context to params. +func (w *ServerInterfaceWrapper) GetClusterConfigFile(ctx echo.Context) error { + var err error + + ctx.Set(BasicAuthScopes, []string{}) + + ctx.Set(BearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetClusterConfigFile(ctx) + return err +} + +// PutClusterConfigFile converts echo context to params. +func (w *ServerInterfaceWrapper) PutClusterConfigFile(ctx echo.Context) error { + var err error + + ctx.Set(BasicAuthScopes, []string{}) + + ctx.Set(BearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.PutClusterConfigFile(ctx) + return err +} + // PostDaemonJoin converts echo context to params. func (w *ServerInterfaceWrapper) PostDaemonJoin(ctx echo.Context) error { var err error @@ -4674,6 +4706,8 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.POST(baseURL+"/cluster/action/abort", wrapper.PostClusterActionAbort) router.POST(baseURL+"/cluster/action/freeze", wrapper.PostClusterActionFreeze) router.POST(baseURL+"/cluster/action/unfreeze", wrapper.PostClusterActionUnfreeze) + router.GET(baseURL+"/cluster/config/file", wrapper.GetClusterConfigFile) + router.PUT(baseURL+"/cluster/config/file", wrapper.PutClusterConfigFile) router.POST(baseURL+"/daemon/action/join", wrapper.PostDaemonJoin) router.POST(baseURL+"/daemon/action/leave", wrapper.PostDaemonLeave) router.POST(baseURL+"/daemon/log/control", wrapper.PostDaemonLogsControl) @@ -4792,215 +4826,215 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "AP//8mMBLHv1AQA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/daemon/daemonapi/get_cluster_config_file.go b/daemon/daemonapi/get_cluster_config_file.go new file mode 100644 index 000000000..3347a1347 --- /dev/null +++ b/daemon/daemonapi/get_cluster_config_file.go @@ -0,0 +1,32 @@ +package daemonapi + +import ( + "net/http" + "time" + + "github.com/labstack/echo/v4" + + "github.com/opensvc/om3/core/naming" + "github.com/opensvc/om3/daemon/api" + "github.com/opensvc/om3/util/file" +) + +func (a *DaemonAPI) GetClusterConfigFile(ctx echo.Context) error { + logName := "GetClusterConfigFile" + log := LogHandler(ctx, logName) + log.Debugf("%s: starting", logName) + + objPath := naming.Cluster + log = naming.LogWithPath(log, objPath) + + filename := objPath.ConfigFile() + mtime := file.ModTime(filename) + if mtime.IsZero() { + log.Infof("%s: config file not found: %s", logName, filename) + return JSONProblemf(ctx, http.StatusNotFound, "Not found", "config file not found: %s", filename) + } + + ctx.Response().Header().Add(api.HeaderLastModifiedNano, mtime.Format(time.RFC3339Nano)) + log.Infof("serve config file %s to %s", objPath, userFromContext(ctx).GetUserName()) + return ctx.File(filename) +} diff --git a/daemon/daemonapi/put_cluster_config_file.go b/daemon/daemonapi/put_cluster_config_file.go new file mode 100644 index 000000000..6ae2e38c4 --- /dev/null +++ b/daemon/daemonapi/put_cluster_config_file.go @@ -0,0 +1,11 @@ +package daemonapi + +import ( + "github.com/labstack/echo/v4" + + "github.com/opensvc/om3/core/naming" +) + +func (a *DaemonAPI) PutClusterConfigFile(ctx echo.Context) error { + return a.writeObjectConfigFile(ctx, naming.Cluster) +} From e35f44ef14b121f1d54f9c45f7d6e388fa27c9ca Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Fri, 25 Oct 2024 08:13:01 +0200 Subject: [PATCH 15/17] Fix a golint error in GET /node/name/.../config/file --- daemon/daemonapi/get_node_config_file.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/daemonapi/get_node_config_file.go b/daemon/daemonapi/get_node_config_file.go index 161ed9ce3..fea290bfd 100644 --- a/daemon/daemonapi/get_node_config_file.go +++ b/daemon/daemonapi/get_node_config_file.go @@ -29,7 +29,7 @@ func (a *DaemonAPI) GetNodeConfigFile(ctx echo.Context, nodename string) error { log.Infof("serve node config file to %s", userFromContext(ctx).GetUserName()) return ctx.File(filename) } - return JSONProblemf(ctx, http.StatusNotFound, "Not found", "Config file not found for %s@%s", a.localhost) + return JSONProblemf(ctx, http.StatusNotFound, "Not found", "Node config file not found") } return a.proxy(ctx, nodename, func(c *client.T) (*http.Response, error) { return c.GetNodeConfigFile(ctx.Request().Context(), nodename) From 6451a5c78fd2e983d2b3cde84b91b521b1920157 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Fri, 25 Oct 2024 10:26:07 +0200 Subject: [PATCH 16/17] Fix the core/monitor/ om mon fixture to follow the node metrics format change. --- core/monitor/testdata/multi-node-om-mon.fixture | 4 ++-- core/monitor/testdata/single-node-om-mon.fixture | 4 ++-- core/tui/main.go | 7 ++++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/core/monitor/testdata/multi-node-om-mon.fixture b/core/monitor/testdata/multi-node-om-mon.fixture index 17c972067..8499a6106 100644 --- a/core/monitor/testdata/multi-node-om-mon.fixture +++ b/core/monitor/testdata/multi-node-om-mon.fixture @@ -11,8 +11,8 @@ Threads node1 node2 node3 Nodes node1 node2 node3 score | 95 96 96 load15m | 0.1 0.2 0.1 - mem | 27/100%:1.94g 24/100%:1.94g 26/100%:1.94g - swap | 2/100%:1.92g 0/100%:1.92g 1/100%:1.92g + mem | 27%1.94g<100% 24%1.94g<100% 26%1.94g<100% + swap | 2%1.92g<100% 0%1.92g<100% 1%1.92g<100% hb-q | 1 0 0 state | diff --git a/core/monitor/testdata/single-node-om-mon.fixture b/core/monitor/testdata/single-node-om-mon.fixture index 53bacead1..fe38ddc87 100644 --- a/core/monitor/testdata/single-node-om-mon.fixture +++ b/core/monitor/testdata/single-node-om-mon.fixture @@ -9,8 +9,8 @@ Threads node3 Nodes node3 score | 96 load15m | 0.1 - mem | 26/100%:1.94g - swap | 0/100%:1.92g + mem | 26%1.94g<100% + swap | 0%1.92g<100% compat warn | 12 state | * diff --git a/core/tui/main.go b/core/tui/main.go index c60a7ea04..f9afcd20b 100644 --- a/core/tui/main.go +++ b/core/tui/main.go @@ -96,7 +96,7 @@ var ( colorNone = tcell.ColorNone colorSelected = tcell.ColorDarkSlateGray colorTitle = tcell.ColorGray - colorHead = tcell.ColorYellow + colorHead = tcell.ColorBlack colorHighlight = tcell.ColorWhite spin = []rune{'⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'} @@ -378,7 +378,12 @@ func (t *App) initApp() { switch event.Key() { case tcell.KeyESC: t.back() + case tcell.KeyBacktab: + t.head.SetBackgroundColor(t.head.GetBackgroundColor() - 1) + t.head.SetText(fmt.Sprintf("set color %d", t.head.GetBackgroundColor())) case tcell.KeyTab: + t.head.SetBackgroundColor(t.head.GetBackgroundColor() + 1) + t.head.SetText(fmt.Sprintf("set color %d", t.head.GetBackgroundColor())) } if t.command != nil { return event From 4b4258c6cb0b1fcf800f2f92c692b14a0a570a43 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Fri, 25 Oct 2024 14:43:57 +0200 Subject: [PATCH 17/17] Beautify tui header Fix key decode --- core/tui/main.go | 104 +++++++++++++++++++++-------------------------- 1 file changed, 46 insertions(+), 58 deletions(-) diff --git a/core/tui/main.go b/core/tui/main.go index f9afcd20b..d0f12c7a4 100644 --- a/core/tui/main.go +++ b/core/tui/main.go @@ -42,7 +42,7 @@ type ( app *tview.Application top *tview.TextView - head *tview.TextView + head *tview.Table errs *tview.TextView textView *tview.TextView keys *tview.Table @@ -96,11 +96,10 @@ var ( colorNone = tcell.ColorNone colorSelected = tcell.ColorDarkSlateGray colorTitle = tcell.ColorGray - colorHead = tcell.ColorBlack + colorHead = tcell.ColorSteelBlue + colorHead2 = tcell.ColorOlive + colorInput = tcell.ColorSteelBlue colorHighlight = tcell.ColorWhite - - spin = []rune{'⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'} - spinLen = len(spin) ) func Run(args []string) error { @@ -122,13 +121,20 @@ func (t viewStack) String() string { } func (t *App) updateHead() { - s := t.stack.String() - if t.focus() == viewObject { - s += " : " + t.Frame.Selector - } else if more := t.selectedString(); more != "" { - s += " : " + more + type titler interface{ GetTitle() string } + if t.flex.GetItemCount() < 2 { + return + } + primitive := t.flex.GetItem(1) + box, ok := primitive.(titler) + if !ok { + return } - t.head.SetText(s) + title := box.GetTitle() + t.head.SetCell(0, 0, tview.NewTableCell(t.Frame.Current.Cluster.Config.Name).SetBackgroundColor(colorHead)) + t.head.SetCell(0, 1, tview.NewTableCell("").SetBackgroundColor(colorHead2).SetTextColor(colorHead)) + t.head.SetCell(0, 2, tview.NewTableCell(title).SetBackgroundColor(colorHead2)) + t.head.SetCell(0, 3, tview.NewTableCell("").SetTextColor(colorHead2)) } func (t viewId) String() string { @@ -231,7 +237,6 @@ func (t *App) initKeysTable() { table.SetBorder(false) onEnter := func(event *tcell.EventKey) { - t.updateKeyTextView() t.nav(viewKey) } @@ -262,6 +267,16 @@ func (t *App) initObjectsTable() { table := tview.NewTable() table.SetEvaluateAllRows(true) + onEnter := func(event *tcell.EventKey) { + switch { + case !t.viewPath.IsZero() && t.viewNode != "": + t.initTextView() + t.nav(viewInstance) + case t.viewPath.Kind == naming.KindCfg || t.viewPath.Kind == naming.KindSec: + t.nav(viewKeys) + } + } + selectedFunc := func(row, col int) { cell := table.GetCell(row, col) path := table.GetCell(row, 0).Text @@ -328,7 +343,7 @@ func (t *App) initObjectsTable() { case tcell.KeyCtrlA: selectAll() case tcell.KeyEnter: - t.onEnter(event) + onEnter(event) return nil // prevents the default select behaviour } switch event.Rune() { @@ -341,7 +356,7 @@ func (t *App) initObjectsTable() { } func (t *App) initHeadTextView() { - t.head = tview.NewTextView() + t.head = tview.NewTable() t.head.SetBorder(false) } @@ -369,7 +384,6 @@ func (t *App) initApp() { t.app = tview.NewApplication() t.flex = tview.NewFlex().SetDirection(tview.FlexRow) t.flex.AddItem(t.head, 1, 0, false) - t.head.SetBackgroundColor(colorHead) t.updateHead() t.flex.AddItem(t.objects, 0, 1, true) t.app.SetRoot(t.flex, true) @@ -379,11 +393,11 @@ func (t *App) initApp() { case tcell.KeyESC: t.back() case tcell.KeyBacktab: - t.head.SetBackgroundColor(t.head.GetBackgroundColor() - 1) - t.head.SetText(fmt.Sprintf("set color %d", t.head.GetBackgroundColor())) + colorHead2-- + t.updateHead() case tcell.KeyTab: - t.head.SetBackgroundColor(t.head.GetBackgroundColor() + 1) - t.head.SetText(fmt.Sprintf("set color %d", t.head.GetBackgroundColor())) + colorHead2++ + t.updateHead() } if t.command != nil { return event @@ -502,6 +516,7 @@ func (t *App) do(statusGetter getter, evReader event.ReadCloser) error { t.Current = *d t.Nodename = data.Daemon.Nodename t.app.QueueUpdateDraw(func() { + t.updateHead() t.updateObjects() }) // show data when new data published on dataC @@ -511,6 +526,7 @@ func (t *App) do(statusGetter getter, evReader event.ReadCloser) error { t.eventCount++ t.app.QueueUpdateDraw(func() { // TODO: detect if t.updateInstanceView and t.updateConfigView need to be called (config mtime change, ...) + t.updateHead() switch t.focus() { case viewInstance: t.updateInstanceView() @@ -623,6 +639,7 @@ func (t *App) updateObjects() { t.lastDraw = time.Now() t.objects.Clear() + t.objects.SetTitle(fmt.Sprintf("%s objects", t.Frame.Selector)) row := 0 t.objects.SetCell(row, 0, tview.NewTableCell("CLUSTER").SetTextColor(colorTitle).SetSelectable(false)) @@ -832,7 +849,7 @@ func (t *App) onRuneColumn(event *tcell.EventKey) { clean := func() { t.flex.RemoveItem(t.command) t.command = nil - t.setFocus() + t.app.SetFocus(t.flex.GetItem(1)) } if t.command != nil { clean() @@ -919,6 +936,7 @@ func (t *App) onRuneColumn(event *tcell.EventKey) { t.command = tview.NewInputField(). SetLabel(":"). SetFieldWidth(0). + SetFieldBackgroundColor(colorInput). SetDoneFunc(func(key tcell.Key) { text := strings.TrimSpace(t.command.GetText()) args := strings.Fields(text) @@ -1017,7 +1035,6 @@ func (t *App) onRuneColumn(event *tcell.EventKey) { func (t *App) setFilter(s string) { t.Frame.Selector = s - t.updateHead() t.restart() } @@ -1409,14 +1426,13 @@ func (t *App) onRuneL(event *tcell.EventKey) { } t.initTextView() - t.nav(viewLog) - t.textView.SetDynamicColors(true) t.textView.SetTitle(title()) + t.textView.SetDynamicColors(true) t.textView.SetChangedFunc(func() { t.textView.ScrollToEnd() }) - t.textView.Clear() + t.nav(viewLog) lines := 50 follow := true @@ -1468,33 +1484,6 @@ func (t *App) onRuneL(event *tcell.EventKey) { }() } -func (t *App) onEnter(event *tcell.EventKey) { - switch { - case !t.viewPath.IsZero() && t.viewNode != "": - t.initTextView() - t.nav(viewInstance) - case t.viewPath.Kind == naming.KindCfg || t.viewPath.Kind == naming.KindSec: - t.nav(viewKeys) - } -} - -func (t *App) setFocus() { - switch t.focus() { - case viewConfig: - t.app.SetFocus(t.textView) - case viewInstance: - t.app.SetFocus(t.textView) - case viewLog: - t.app.SetFocus(t.textView) - case viewKeys: - t.app.SetFocus(t.keys) - case viewKey: - t.app.SetFocus(t.textView) - default: - t.app.SetFocus(t.objects) - } -} - func (t *App) getConfigUpdatedAt() time.Time { path := t.viewPath.String() for _, nodeData := range t.Current.Cluster.Node { @@ -1550,6 +1539,7 @@ func (t *App) updateKeysView() { return } t.keys.Clear() + t.keys.SetTitle(fmt.Sprintf("%s keys", t.viewPath)) t.keys.SetCell(0, 0, tview.NewTableCell("NAME").SetTextColor(colorTitle).SetSelectable(false)) t.keys.SetCell(0, 1, tview.NewTableCell("SIZE").SetTextColor(colorTitle).SetSelectable(false)) for i, key := range resp.JSON200.Items { @@ -1641,10 +1631,9 @@ func (t *App) updateClusterConfigView() { } text := tview.TranslateANSI(string(resp.Body)) - title := fmt.Sprintf("cluster configuration") t.textView.SetDynamicColors(false) - t.textView.SetTitle(title) t.textView.Clear() + t.textView.SetTitle("cluster configuration") fmt.Fprint(t.textView, text) } @@ -1662,9 +1651,8 @@ func (t *App) updateNodeConfigView() { } text := tview.TranslateANSI(string(resp.Body)) - title := fmt.Sprintf("%s configuration", t.viewPath) t.textView.SetDynamicColors(false) - t.textView.SetTitle(title) + t.textView.SetTitle(fmt.Sprintf("%s configuration", t.viewNode)) t.textView.Clear() fmt.Fprint(t.textView, text) } @@ -1682,9 +1670,8 @@ func (t *App) updateObjectConfigView() { } text := tview.TranslateANSI(string(resp.Body)) - title := fmt.Sprintf("%s configuration", t.viewPath) t.textView.SetDynamicColors(false) - t.textView.SetTitle(title) + t.textView.SetTitle(fmt.Sprintf("%s configuration", t.viewPath.String())) t.textView.Clear() fmt.Fprint(t.textView, text) } @@ -1746,7 +1733,6 @@ func (t *App) navFromTo(from, to viewId) { case viewKeys: t.keys = nil } - t.updateHead() switch to { case viewLog: t.initTextView() @@ -1760,6 +1746,7 @@ func (t *App) navFromTo(from, to viewId) { t.initTextView() t.flex.AddItem(t.textView, 0, 1, true) t.app.SetFocus(t.textView) + t.updateKeyTextView() case viewInstance: t.initTextView() t.flex.AddItem(t.textView, 0, 1, true) @@ -1775,6 +1762,7 @@ func (t *App) navFromTo(from, to viewId) { t.app.SetFocus(t.objects) t.updateObjects() } + t.updateHead() t.flex.AddItem(t.errs, 1, 0, false) }