From 68d1043051d9e83779b6bd303c9600a783c5bf84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Moti=C4=8D=C3=A1k?= Date: Thu, 6 Jul 2023 21:26:46 +0200 Subject: [PATCH 1/5] Implement swctl status command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Peter Motičák --- client/client.go | 202 +++++++++++++++++++++++++++- client/component.go | 102 ++++++++++++-- client/http.go | 258 ++++++++++++++++++++++++++++++++++++ cmd/swctl/cli.go | 26 +++- cmd/swctl/cmd_status.go | 245 ++++++++++++++++++++++++++++++++-- go.mod | 6 +- plugins/cnfreg/discovery.go | 13 +- plugins/cnfreg/options.go | 2 + plugins/cnfreg/plugin.go | 21 +-- plugins/cnfreg/rest.go | 81 +++++++++++ plugins/puntmgr/rest.go | 36 +++++ 11 files changed, 947 insertions(+), 45 deletions(-) create mode 100644 client/http.go create mode 100644 plugins/cnfreg/rest.go create mode 100644 plugins/puntmgr/rest.go diff --git a/client/client.go b/client/client.go index 12bbb86f..00cb8c06 100644 --- a/client/client.go +++ b/client/client.go @@ -1,11 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2021 PANTHEON.tech +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package client import ( + "context" + "crypto/tls" + "encoding/json" "fmt" + "net/http" + "strings" + "time" + + docker "github.com/fsouza/go-dockerclient" + "github.com/sirupsen/logrus" + vppagent "go.ligato.io/vpp-agent/v3/cmd/agentctl/client" + "go.ligato.io/vpp-agent/v3/cmd/agentctl/client/tlsconfig" + linux_nsplugin "go.ligato.io/vpp-agent/v3/plugins/linux/nsplugin" + + "go.pantheon.tech/stonework/plugins/cnfreg" +) + +const ( + DefaultHost = "127.0.0.1" + DefaultHTTPClientTimeout = 60 * time.Second + DefaultPortGRPC = 9991 + DefaultPortHTTP = 9191 + DockerComposeServiceLabel = "com.docker.compose.service" ) // Option is a function that customizes a Client. -type Option func(*Client) +type Option func(*Client) error + +func WithHost(h string) Option { + return func(c *Client) error { + c.host = h + return nil + } +} + +func WithHTTPPort(p uint16) Option { + return func(c *Client) error { + c.httpPort = p + return nil + } +} + +func WithHTTPTLS(cert, key, ca string, skipVerify bool) Option { + return func(c *Client) (err error) { + c.httpTLS, err = withTLS(cert, key, ca, skipVerify) + return err + } +} // API defines client API. It is supposed to be used by various client // applications, such as swctl or other user applications interacting with @@ -16,25 +75,154 @@ type API interface { // Client implements API interface. type Client struct { - components []Component + nsPlugin *linux_nsplugin.NsPlugin + dockerClient *docker.Client + httpClient *http.Client + host string + scheme string + protocol string + httpPort uint16 + httpTLS *tls.Config + customHTTPHeaders map[string]string } // NewClient creates a new client that implements API. The client can be // customized by options. func NewClient(opts ...Option) (*Client, error) { - c := &Client{} + c := &Client{ + host: DefaultHost, + scheme: "http", + protocol: "tcp", + httpPort: DefaultPortHTTP, + } + var err error + c.dockerClient, err = docker.NewClientFromEnv() + if err != nil { + return nil, err + } for _, o := range opts { o(c) } + return c, nil } -// GetComponents returns list of components. +func (c *Client) DockerClient() *docker.Client { + return c.dockerClient +} + +// HTTPClient returns configured HTTP client. +func (c *Client) HTTPClient() *http.Client { + if c.httpClient == nil { + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.TLSClientConfig = c.httpTLS + c.httpClient = &http.Client{ + Transport: tr, + Timeout: DefaultHTTPClientTimeout, + } + } + return c.httpClient +} + +func withTLS(cert, key, ca string, skipVerify bool) (*tls.Config, error) { + var options []tlsconfig.Option + if cert != "" && key != "" { + options = append(options, tlsconfig.CertKey(cert, key)) + } + if ca != "" { + options = append(options, tlsconfig.CA(ca)) + } + if skipVerify { + options = append(options, tlsconfig.SkipServerVerification()) + } + return tlsconfig.New(options...) +} + +func (c *Client) StatusInfo(ctx context.Context) ([]cnfreg.Info, error) { + resp, err := c.get(ctx, "/status/info", nil, nil) + if err != nil { + return nil, fmt.Errorf("HTTP request failed: %w", err) + } + var infos []cnfreg.Info + if err := json.NewDecoder(resp.body).Decode(&infos); err != nil { + return nil, fmt.Errorf("decoding reply failed: %w", err) + } + return infos, nil +} + func (c *Client) GetComponents() ([]Component, error) { - var components []Component + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + infos, err := c.StatusInfo(ctx) + if err != nil { + return nil, err + } + + dc := c.DockerClient() + containerInfo, err := dc.ListContainers(docker.ListContainersOptions{}) + if err != nil { + return nil, err + } - // TODO: implement retrieval of components + var containers []*docker.Container + for _, container := range containerInfo { + c, err := dc.InspectContainerWithOptions(docker.InspectContainerOptions{ID: container.ID}) + if err != nil { + return nil, err + } + containers = append(containers, c) + logrus.Warnf("[WLL] container name: %s, PID: %d, ENV: %+v", c.Name, c.State.Pid, c.Config.Env) + } + logrus.Warnf("[WLL] number of containers: &d", len(containers)) - return components, fmt.Errorf("NOT IMPLEMENTED YET") + cnfInfos := make(map[string]cnfreg.Info) + for _, info := range infos { + cnfInfos[info.MsLabel] = info + } + var components []Component + var foreignContainers []*docker.Container + for _, container := range containers { + after, found := containsPrefix(container.Config.Env, "MICROSERVICE_LABEL=") + if !found { + foreignContainers = append(foreignContainers, container) + continue + } + info, ok := cnfInfos[after] + if !ok { + foreignContainers = append(foreignContainers, container) + continue + } + client, err := vppagent.NewClientWithOpts(vppagent.WithHost(info.IPAddr), vppagent.WithHTTPPort(info.HTTPPort)) + if err != nil { + return components, err + } + compo := &component{ + agentclient: client, + Name: info.MsLabel, + Mode: cnfModeToCompoMode(info.Mode), + Info: &info, + } + components = append(components, compo) + } + + for _, fcontainer := range foreignContainers { + compo := &component{ + Name: fcontainer.Config.Labels[DockerComposeServiceLabel], + Mode: ComponentForeign, + } + components = append(components, compo) + } + return components, nil } + +func containsPrefix(strs []string, prefix string) (string, bool) { + for _, str := range strs { + found := strings.HasPrefix(str, prefix) + if found { + return strings.TrimPrefix(str, prefix), found + } + } + return "", false +} \ No newline at end of file diff --git a/client/component.go b/client/component.go index e68e5def..7cc24816 100644 --- a/client/component.go +++ b/client/component.go @@ -1,23 +1,107 @@ package client -import "go.pantheon.tech/stonework/proto/cnfreg" +import ( + "context" + "fmt" + + "go.ligato.io/cn-infra/v2/health/probe" + "go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types" + "go.ligato.io/vpp-agent/v3/cmd/agentctl/client" + + "go.ligato.io/vpp-agent/v3/proto/ligato/kvscheduler" + "go.pantheon.tech/stonework/plugins/cnfreg" + cnfregpb "go.pantheon.tech/stonework/proto/cnfreg" +) + +type ComponentMode int32 + +const ( + // Foreign means the component is not managed by StoneWork + ComponentForeign ComponentMode = iota + + // Stonework means the component is a StoneWork instance + ComponentStonework + + // StoneworkModule means the component is a StoneWork module managed by StoneWork + ComponentStoneworkModule +) // Component is a component of StoneWork. It can be StoneWork instance itself, // a CNF connected to it or other Ligato service in connected to StoneWork. type Component interface { - Name() string - Mode() cnfreg.CnfMode + // Client() *client.Client + GetName() string + GetMode() ComponentMode + GetInfo() *cnfreg.Info + GetMetadata() map[string]string + SchedulerValues() ([]*kvscheduler.BaseValueStatus, error) } type component struct { - name string - mode cnfreg.CnfMode + agentclient *client.Client + Name string + Mode ComponentMode + Info *cnfreg.Info + Metadata map[string]string +} + +func (c *component) GetName() string { + return c.Name +} + +func (c *component) GetMode() ComponentMode { + return c.Mode } -func (c *component) Name() string { - return c.name +func (c *component) GetInfo() *cnfreg.Info { + return c.Info } -func (c *component) Mode() cnfreg.CnfMode { - return c.mode +func (c *component) Client() *client.Client { + return c.agentclient } + +func (c *component) GetMetadata() map[string]string { + return c.Metadata +} + +func (c *component) SchedulerValues() ([]*kvscheduler.BaseValueStatus, error) { + if c.Mode == ComponentForeign { + return nil, fmt.Errorf("cannot get scheduler values of component %s, this component in not managed by StoneWork", c.Name) + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + values, err := c.agentclient.SchedulerValues(ctx, types.SchedulerValuesOptions{}) + if err != nil { + return nil, err + } + return values, nil +} + +func (c *component) Readiness() (*probe.ExposedStatus, error) { + if c.Mode == ComponentForeign { + return nil, fmt.Errorf("cannot get readiness of component %s, this component in not managed by StoneWork", c.Name) + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + status, err := c.agentclient.Status(ctx) + if err != nil { + return nil, err + } + return status, nil +} + +func cnfModeToCompoMode(cm cnfregpb.CnfMode) ComponentMode { + switch cm { + case cnfregpb.CnfMode_STANDALONE: + return ComponentForeign + case cnfregpb.CnfMode_STONEWORK_MODULE: + return ComponentStoneworkModule + case cnfregpb.CnfMode_STONEWORK: + return ComponentStonework + default: + return ComponentForeign + } +} \ No newline at end of file diff --git a/client/http.go b/client/http.go new file mode 100644 index 00000000..45d8f7f2 --- /dev/null +++ b/client/http.go @@ -0,0 +1,258 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "strconv" + "strings" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types" +) + +// serverResponse is a wrapper for http API responses. +type serverResponse struct { + body io.ReadCloser + contentLen int64 + header http.Header + statusCode int + reqURL *url.URL +} + +func (c *Client) get(ctx context.Context, path string, query url.Values, headers map[string][]string) (serverResponse, error) { + return c.sendRequest(ctx, "GET", path, query, nil, headers) +} + +func (c *Client) post(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) { + body, headers, err := encodeBody(obj, headers) + if err != nil { + return serverResponse{}, err + } + return c.sendRequest(ctx, "POST", path, query, body, headers) +} + +func (c *Client) put(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) { + body, headers, err := encodeBody(obj, headers) + if err != nil { + return serverResponse{}, err + } + return c.sendRequest(ctx, "PUT", path, query, body, headers) +} + +type headers map[string][]string + +func encodeBody(obj interface{}, headers headers) (io.Reader, headers, error) { + if obj == nil { + return nil, headers, nil + } + + body, err := encodeData(obj) + if err != nil { + return nil, headers, err + } + if headers == nil { + headers = make(map[string][]string) + } + headers["Content-Type"] = []string{"application/json"} + return body, headers, nil +} + +func (c *Client) buildRequest(method, path string, body io.Reader, headers headers) (*http.Request, error) { + expectedPayload := method == "POST" || method == "PUT" + if expectedPayload && body == nil { + body = bytes.NewReader([]byte{}) + } + + req, err := http.NewRequest(method, path, body) + if err != nil { + return nil, err + } + req = c.addHeaders(req, headers) + + if c.protocol == "unix" || c.protocol == "npipe" { + // For local communications, it doesn't matter what the host is. + // We just need a valid and meaningful host name. + req.Host = "stonework-agent" + } + + req.URL.Host = net.JoinHostPort(c.host, strconv.Itoa(int(c.httpPort))) + req.URL.Scheme = c.scheme + + if expectedPayload && req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "text/plain") + } + return req, nil +} + +func (c *Client) sendRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers headers) (serverResponse, error) { + fullPath := (&url.URL{Path: path, RawQuery: query.Encode()}).String() + req, err := c.buildRequest(method, fullPath, body, headers) + if err != nil { + return serverResponse{}, err + } + resp, err := c.doRequest(ctx, req) + if err != nil { + return resp, err + } + err = c.checkResponseErr(resp) + return resp, err +} + +func (c *Client) doRequest(ctx context.Context, req *http.Request) (serverResponse, error) { + serverResp := serverResponse{ + statusCode: -1, + reqURL: req.URL, + } + var ( + err error + resp *http.Response + ) + req = req.WithContext(ctx) + + fields := map[string]interface{}{} + if req.ContentLength > 0 { + fields["contentLength"] = req.ContentLength + } + logrus.WithFields(fields).Debugf("=> sending http request: %s %s", req.Method, req.URL) + defer func() { + if err != nil { + logrus.Debugf("<- http response ERROR: %v", err) + } else { + logrus.Debugf("<- http response %v (%d bytes)", serverResp.statusCode, serverResp.contentLen) + } + }() + + resp, err = c.HTTPClient().Do(req) + if err != nil { + if c.scheme != "https" && strings.Contains(err.Error(), "malformed HTTP response") { + return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err) + } + if c.scheme == "https" && strings.Contains(err.Error(), "bad certificate") { + return serverResp, errors.Wrap(err, "The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings") + } + + // Don't decorate context sentinel errors; users may be comparing to + // them directly. + switch err { + case context.Canceled, context.DeadlineExceeded: + return serverResp, err + } + if nErr, ok := err.(*url.Error); ok { + if nErr, ok := nErr.Err.(*net.OpError); ok { + if os.IsPermission(nErr.Err) { + return serverResp, errors.Wrapf(err, "Got permission denied while trying to connect to the agent socket at %v", c.host) + } + } + } + if err, ok := err.(net.Error); ok { + if err.Timeout() { + return serverResp, fmt.Errorf("cannot connect to StoneWork at %s", c.host) + } + if !err.Temporary() { + if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "dial unix") { + return serverResp, fmt.Errorf("cannot connect to StoneWork at %s", c.host) + } + } + } + return serverResp, errors.Wrap(err, "error during connect") + } + if logrus.IsLevelEnabled(logrus.DebugLevel) { + body, err := io.ReadAll(resp.Body) + if err != nil { + logrus.Debugf("reading body failed: %v", err) + } else { + logrus.Debugf("body: %s", body) + } + resp.Body = io.NopCloser(bytes.NewReader(body)) + } + if resp != nil { + serverResp.statusCode = resp.StatusCode + serverResp.body = resp.Body + serverResp.header = resp.Header + serverResp.contentLen = resp.ContentLength + } + return serverResp, nil +} + +func (c *Client) checkResponseErr(serverResp serverResponse) error { + if serverResp.statusCode >= 200 && serverResp.statusCode < 400 { + return nil + } + var body []byte + var err error + if serverResp.body != nil { + bodyMax := 1 * 1024 * 1024 // 1 MiB + bodyR := &io.LimitedReader{ + R: serverResp.body, + N: int64(bodyMax), + } + body, err = io.ReadAll(bodyR) + if err != nil { + return err + } + if bodyR.N == 0 { + return fmt.Errorf("request returned %s with a message (> %d bytes) for API route and version %s, check if the server supports the requested API version", + http.StatusText(serverResp.statusCode), bodyMax, serverResp.reqURL) + } + } + if len(body) == 0 { + return fmt.Errorf("request returned %s for API route and version %s, check if the server supports the requested API version", + http.StatusText(serverResp.statusCode), serverResp.reqURL) + } + var ct string + if serverResp.header != nil { + ct = serverResp.header.Get("Content-Type") + } + var errorMsg string + if ct == "application/json" { + var errorResponse types.ErrorResponse + if err := json.Unmarshal(body, &errorResponse); err != nil { + return errors.Wrap(err, "Error unmarshaling JSON body") + } + errorMsg = errorResponse.Message + } else { + errorMsg = string(body) + } + errorMsg = fmt.Sprintf("[%d] %s", serverResp.statusCode, strings.TrimSpace(errorMsg)) + + return errors.Wrap(errors.New(errorMsg), "Error response from daemon") +} + +func (c *Client) addHeaders(req *http.Request, headers headers) *http.Request { + // Add CLI Config's HTTP Headers BEFORE we set the client headers + // then the user can't change OUR headers + for k, v := range c.customHTTPHeaders { + req.Header.Set(k, v) + } + for k, v := range headers { + req.Header[k] = v + } + return req +} + +func encodeData(data interface{}) (*bytes.Buffer, error) { + params := bytes.NewBuffer(nil) + if data != nil { + if err := json.NewEncoder(params).Encode(data); err != nil { + return nil, err + } + } + return params, nil +} + +func ensureReaderClosed(response serverResponse) { + if response.body != nil { + // Drain up to 512 bytes and close the body to let the Transport reuse the connection + _, _ = io.CopyN(io.Discard, response.body, 512) + _ = response.body.Close() + } +} diff --git a/cmd/swctl/cli.go b/cmd/swctl/cli.go index 8ef5088f..45d4b1d4 100644 --- a/cmd/swctl/cli.go +++ b/cmd/swctl/cli.go @@ -10,6 +10,8 @@ import ( "github.com/moby/term" "github.com/sirupsen/logrus" + // agentcli "go.ligato.io/vpp-agent/v3/cmd/agentctl/cli" + "go.pantheon.tech/stonework/client" ) @@ -64,7 +66,8 @@ func NewCli(opt ...CliOption) (*CLI, error) { } func (cli *CLI) Initialize(opts Options) (err error) { - cli.client, err = initClient(opts) + // clientCfg, err := agentcli.MakeConfig() + cli.client, err = initClient() if err != nil { return fmt.Errorf("init error: %w", err) } @@ -86,8 +89,25 @@ func (cli *CLI) Initialize(opts Options) (err error) { return nil } -func initClient(opts Options) (*client.Client, error) { - c, err := client.NewClient() +func initClient() (*client.Client, error) { + // cfg, err := agentcli.MakeConfig() + // if err != nil { + // return nil, err + // } + opts := []client.Option{ + // client.WithHost(cfg.Host), + // client.WithGRPCPort(uint16(cfg.GRPCPort)), + // client.WithHTTPPort(uint16(cfg.HTTPPort)), + } + // if cfg.ShouldUseSecureGRPC() { + // opts = append(opts, client.WithGRPCTLS( + // cfg.GRPCSecure.CertFile, + // cfg.GRPCSecure.KeyFile, + // cfg.GRPCSecure.CAFile, + // cfg.GRPCSecure.SkipVerify, + // )) + // } + c, err := client.NewClient(opts...) if err != nil { return nil, err } diff --git a/cmd/swctl/cmd_status.go b/cmd/swctl/cmd_status.go index 882e8a83..991b05db 100644 --- a/cmd/swctl/cmd_status.go +++ b/cmd/swctl/cmd_status.go @@ -2,10 +2,20 @@ package main import ( "fmt" - "os/exec" + "io" + "strconv" + "strings" + "sync" "github.com/gookit/color" + "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" + "golang.org/x/exp/constraints" + "golang.org/x/exp/slices" + + "go.ligato.io/vpp-agent/v3/proto/ligato/kvscheduler" + + "go.pantheon.tech/stonework/client" ) // TODO: improve status overview, show status of components (CNFs) @@ -20,7 +30,8 @@ const statusExample = ` ` type StatusCmdOptions struct { - Args []string + Args []string + Format string } func NewStatusCmd(cli Cli) *cobra.Command { @@ -41,16 +52,234 @@ func NewStatusCmd(cli Cli) *cobra.Command { return cmd } +type statusInfo struct { + client.Component + ConfigCounts configCounts +} + func runStatusCmd(cli Cli, opts StatusCmdOptions) error { - cmd := fmt.Sprintf("vpp-probe --env=%s discover", defaultVppProbeEnv) - out, err := cli.Exec(cmd, opts.Args) + resp, err := cli.Client().GetComponents() if err != nil { - if ee, ok := err.(*exec.ExitError); ok { - return fmt.Errorf("%v: %s", ee.String(), ee.Stderr) - } return err } - fmt.Fprintln(cli.Out(), out) + type infoWithErr struct { + statusInfo + error + } + var infos []statusInfo + var wg sync.WaitGroup + infoCh := make(chan infoWithErr) + + type fetched struct { + values []*kvscheduler.BaseValueStatus + err error + } + + for _, compo := range resp { + wg.Add(1) + go func(compo client.Component) { + defer wg.Done() + var counts configCounts + if compo.GetMode() != client.ComponentForeign { + vals, err := compo.SchedulerValues() + if err != nil { + infoCh <- infoWithErr{error: err} + } + counts = countConfig(vals) + } + infoCh <- infoWithErr{ + statusInfo: statusInfo{ + Component: compo, + ConfigCounts: counts, + }, + } + }(compo) + } + + go func() { + wg.Wait() + close(infoCh) + }() + + for i := range infoCh { + if i.error != nil { + return i.error + } + infos = append(infos, i.statusInfo) + } + slices.SortFunc(infos, cmpInfos) + // if err := formatAsTemplate(cli.Out(), "json", resp); err != nil { + // return err + // } + printStatusTable(cli.Out(), infos) return nil } + +func countConfig(baseVals []*kvscheduler.BaseValueStatus) configCounts { + var allVals []*kvscheduler.ValueStatus + for _, baseVal := range baseVals { + allVals = append(allVals, baseVal.Value) + allVals = append(allVals, baseVal.DerivedValues...) + } + + var res configCounts + for _, val := range allVals { + switch val.State { + case kvscheduler.ValueState_INVALID, kvscheduler.ValueState_FAILED: + res.Err++ + case kvscheduler.ValueState_MISSING: + res.Missing++ + case kvscheduler.ValueState_PENDING: + res.Pending++ + case kvscheduler.ValueState_RETRYING: + res.Retrying++ + case kvscheduler.ValueState_UNIMPLEMENTED: + res.Unimplemented++ + case kvscheduler.ValueState_CONFIGURED, kvscheduler.ValueState_DISCOVERED, kvscheduler.ValueState_OBTAINED, kvscheduler.ValueState_REMOVED, kvscheduler.ValueState_NONEXISTENT: + res.Ok++ + } + } + return res +} + +type comparable interface { + constraints.Integer | ~string +} + +func less[T comparable](a, b T) bool { + if a > b { + return true + } + return false +} + +func cmpInfos(a, b statusInfo) bool { + res := less(a.GetMode(), b.GetMode()) + bLessA := less(b.GetMode(), a.GetMode()) + if !(res || bLessA) { + res = less(a.GetName(), b.GetName()) + } + return res +} + +func printStatusTable(out io.Writer, infos []statusInfo) { + table := tablewriter.NewWriter(out) + header := []string{ + "Name", "Mode", "IP Address", "GPRC Port", "HTTP Port", "Status", "Configuration", + } + aleft := tablewriter.ALIGN_LEFT + acenter := tablewriter.ALIGN_CENTER + table.SetHeader(header) + table.SetAutoWrapText(false) + table.SetAutoFormatHeaders(true) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetColumnAlignment([]int{aleft, aleft, aleft, acenter, acenter, acenter, aleft}) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetTablePadding("\t") + for _, info := range infos { + row := []string{info.GetName(), compoModeString(info.GetMode())} + var clrs []tablewriter.Colors + if info.GetMode() == client.ComponentForeign { + clrs = []tablewriter.Colors{{}, {}} + for i := range header[2:] { + clrs = append(clrs, []int{tablewriter.FgHiBlackColor}) + row = append(row, strings.Repeat("-", len(header[i+2]))) + } + table.Rich(row, clrs) + continue + } + config := info.ConfigCounts.String() + configColor := info.ConfigCounts.Color() + compoInfo := info.GetInfo() + grpcState := compoInfo.GRPCConnState.String() + var statusClr int + // gRPC state does not make sense for StoneWork itself + if info.GetMode() == client.ComponentStonework { + grpcState = strings.Repeat("-", len("Status")) + statusClr = tablewriter.FgHiBlackColor + } + row = append(row, + compoInfo.IPAddr, + strconv.Itoa(compoInfo.GRPCPort), + strconv.Itoa(compoInfo.HTTPPort), + grpcState, + config) + clrs = []tablewriter.Colors{ + {}, {}, {}, {}, {}, {statusClr}, {configColor}, + } + table.Rich(row, clrs) + } + table.Render() +} + +func compoModeString(c client.ComponentMode) string { + switch c { + case client.ComponentForeign: + return "foreign" + case client.ComponentStonework: + return "StoneWork" + case client.ComponentStoneworkModule: + return "StoneWork module" + } + return "unknown" +} + +type configCounts struct { + Ok int + Err int + Missing int + Pending int + Retrying int + Unimplemented int +} + +func (c configCounts) String() string { + var fields []string + if c.Ok != 0 { + fields = append(fields, fmt.Sprintf("%d OK", c.Ok)) + } + if c.Err != 0 { + errStr := fmt.Sprintf("%d errors", c.Ok) + if c.Err == 1 { + errStr = errStr[:len(errStr)-1] + } + fields = append(fields, errStr) + } + if c.Missing != 0 { + fields = append(fields, fmt.Sprintf("%d missing", c.Missing)) + } + if c.Pending != 0 { + fields = append(fields, fmt.Sprintf("%d pending", c.Pending)) + } + if c.Retrying != 0 { + fields = append(fields, fmt.Sprintf("%d retrying", c.Retrying)) + } + if c.Unimplemented != 0 { + fields = append(fields, fmt.Sprintf("%d unimplemented", c.Unimplemented)) + } + return strings.Join(fields, ", ") +} + +func (c configCounts) Color() int { + if c.Err > 0 { + return tablewriter.FgHiRedColor + } + if c.Retrying > 0 || c.Pending > 0 { + return tablewriter.FgYellowColor + } + if c.Unimplemented > 0 { + return tablewriter.FgMagentaColor + } + if c.Missing > 0 { + return tablewriter.FgHiYellowColor + } + if c.Ok > 0 { + return tablewriter.FgGreenColor + } + return 0 +} \ No newline at end of file diff --git a/go.mod b/go.mod index 63a1eccf..4019be1b 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/fatih/color v1.10.0 // indirect github.com/fluent/fluent-logger-golang v1.3.0 // indirect github.com/fogleman/gg v1.3.0 // indirect - github.com/fsouza/go-dockerclient v1.7.1 // indirect + github.com/fsouza/go-dockerclient v1.7.1 github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff // indirect github.com/goccy/go-graphviz v0.0.6 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -82,7 +82,7 @@ require ( github.com/moby/sys/mount v0.2.0 // indirect github.com/moby/sys/mountinfo v0.6.2 // indirect github.com/morikuni/aec v1.0.0 // indirect - github.com/olekukonko/tablewriter v0.0.4 // indirect + github.com/olekukonko/tablewriter v0.0.4 github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/runc v1.1.2 // indirect @@ -99,7 +99,7 @@ require ( github.com/spf13/viper v1.7.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect github.com/tinylib/msgp v1.0.2 // indirect - github.com/unrolled/render v0.0.0-20180914162206-b9786414de4d // indirect + github.com/unrolled/render v0.0.0-20180914162206-b9786414de4d github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.1.0 // indirect diff --git a/plugins/cnfreg/discovery.go b/plugins/cnfreg/discovery.go index c4e5cd6b..8677e69d 100644 --- a/plugins/cnfreg/discovery.go +++ b/plugins/cnfreg/discovery.go @@ -135,22 +135,23 @@ func (p *Plugin) loadSwModFromFile(fpath string) (swModule, error) { if err != nil { return swMod, fmt.Errorf("failed to parse PID file %s: %v", fname, err) } - swMod, err = p.getCnfModels(pf.IpAddress, pf.GrpcPort, pf.HttpPort) + swMod, err = p.getCnfModels(pf) if err != nil { return swMod, fmt.Errorf("failed to obtain CNF models (pid file: %v): %v", fname, err) } return swMod, nil } -func (p *Plugin) getCnfModels(ipAddress string, grpcPort, httpPort int) (swMod swModule, err error) { - swMod.ipAddress = ipAddress - swMod.grpcPort = grpcPort - swMod.httpPort = httpPort +func (p *Plugin) getCnfModels(pf PidFile) (swMod swModule, err error) { + swMod.pid = pf.Pid + swMod.ipAddress = pf.IpAddress + swMod.grpcPort = pf.GrpcPort + swMod.httpPort = pf.HttpPort // connect to the SW-Module CNF over gRPC ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - swMod.grpcConn, err = grpc.DialContext(ctx, fmt.Sprintf("%s:%d", ipAddress, grpcPort), + swMod.grpcConn, err = grpc.DialContext(ctx, fmt.Sprintf("%s:%d", pf.IpAddress, pf.GrpcPort), grpc.WithBlock(), grpc.WithInsecure()) if err != nil { return swMod, err diff --git a/plugins/cnfreg/options.go b/plugins/cnfreg/options.go index 8ef9e3f4..2c43fdb3 100644 --- a/plugins/cnfreg/options.go +++ b/plugins/cnfreg/options.go @@ -22,6 +22,7 @@ import ( "go.ligato.io/cn-infra/v2/config" "go.ligato.io/cn-infra/v2/logging" "go.ligato.io/cn-infra/v2/rpc/grpc" + "go.ligato.io/cn-infra/v2/rpc/rest" "go.ligato.io/cn-infra/v2/servicelabel" "go.ligato.io/vpp-agent/v3/plugins/kvscheduler" @@ -58,6 +59,7 @@ func NewPlugin(opts ...Option) *Plugin { p.MgmtSubnet = os.Getenv(CnfMgmtSubnetEnvVar) p.ServiceLabel = &servicelabel.DefaultPlugin p.KVScheduler = &kvscheduler.DefaultPlugin + p.HTTPPlugin = &rest.DefaultPlugin p.GRPCPlugin = &grpc.DefaultPlugin // Note: Punt Manager not injected by default due to a cyclical dependency between these two plugins diff --git a/plugins/cnfreg/plugin.go b/plugins/cnfreg/plugin.go index 8245a058..997df5a4 100644 --- a/plugins/cnfreg/plugin.go +++ b/plugins/cnfreg/plugin.go @@ -168,14 +168,16 @@ type swAttrs struct { // CNF used as a StoneWork Module. type swModule struct { + pid int cnfMsLabel string ipAddress string grpcPort int httpPort int - grpcConn grpc.ClientConnInterface - cnfClient pb.CnfDiscoveryClient - cfgClient client.GenericClient - cnfModels []cnfModel + grpcConn *grpc.ClientConn + // grpcConn grpc.ClientConnInterface + cnfClient pb.CnfDiscoveryClient + cfgClient client.GenericClient + cnfModels []cnfModel } // Attributes specific to StoneWork Module (i.e. not used by standalone CNF or StoneWork itself). @@ -242,6 +244,10 @@ func (p *Plugin) Init() (err error) { p.Log.Infof("Discovered management IP address: %v", p.ipAddress) } + grpcServer := p.GRPCPlugin.GetServer() + if grpcServer == nil { + return errors.New("gRPC server is not initialized") + } switch p.cnfMode { case pb.CnfMode_STONEWORK_MODULE: // inject gRPC and HTTP ports to use by SW-Module @@ -251,14 +257,11 @@ func (p *Plugin) Init() (err error) { } // serve CnfDiscovery methods - grpcServer := p.GRPCPlugin.GetServer() - if grpcServer == nil { - return errors.New("gRPC server is not initialized") - } pb.RegisterCnfDiscoveryServer(grpcServer, p) case pb.CnfMode_STONEWORK: p.sw.modules = conc.NewMap[string, swModule]() + p.registerHandlers(p.HTTPPlugin) // CNF discovery go p.cnfDiscovery(make(chan struct{})) } @@ -296,7 +299,7 @@ func (p *Plugin) GetGrpcPort() (port int) { } // Returns gRPC port that should be used by this CNF. -// Not to be used by StoneWork or a standalone CFN (they should respect what is in http.conf). +// Not to be used by StoneWork or a standalone CNF (they should respect what is in http.conf). func (p *Plugin) GetHttpPort() (port int) { if p.cnfMode != pb.CnfMode_STONEWORK_MODULE { panic(fmt.Errorf("method GetHttpPort is not available in the CNF mode %v", p.cnfMode)) diff --git a/plugins/cnfreg/rest.go b/plugins/cnfreg/rest.go new file mode 100644 index 00000000..e6729340 --- /dev/null +++ b/plugins/cnfreg/rest.go @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 PANTHEON.tech +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cnfreg + +import ( + "net/http" + "os" + + "github.com/unrolled/render" + "go.ligato.io/cn-infra/v2/rpc/rest" + "google.golang.org/grpc/connectivity" + + pb "go.pantheon.tech/stonework/proto/cnfreg" +) + +type Info struct { + PID int + MsLabel string + Mode pb.CnfMode + IPAddr string + GRPCPort int + HTTPPort int + GRPCConnState connectivity.State +} + +func (p *Plugin) registerHandlers(handlers rest.HTTPHandlers) { + if handlers == nil { + p.Log.Debug("No http handler provided, skipping registration of REST handlers") + return + } + if p.cnfMode == pb.CnfMode_STONEWORK { + handlers.RegisterHTTPHandler("/status/info", p.statusHandler, http.MethodGet) + } +} + +func (p *Plugin) statusHandler(formatter *render.Render) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + var infos []*Info + + swInfo := &Info{ + PID: os.Getpid(), + MsLabel: p.ServiceLabel.GetAgentLabel(), + Mode: pb.CnfMode_STONEWORK, + IPAddr: p.ipAddress.String(), + GRPCPort: p.GRPCPlugin.GetPort(), + HTTPPort: p.HTTPPlugin.GetPort(), + } + infos = append(infos, swInfo) + + for kv := range p.sw.modules.Iter() { + swMod := kv.Val + swModInfo := &Info{ + PID: swMod.pid, + MsLabel: swMod.cnfMsLabel, + Mode: pb.CnfMode_STONEWORK_MODULE, + IPAddr: swMod.ipAddress, + GRPCPort: swMod.grpcPort, + HTTPPort: swMod.httpPort, + GRPCConnState: swMod.grpcConn.GetState(), + } + infos = append(infos, swModInfo) + } + if err := formatter.JSON(w, http.StatusOK, infos); err != nil { + p.Log.Error(err) + } + } +} \ No newline at end of file diff --git a/plugins/puntmgr/rest.go b/plugins/puntmgr/rest.go new file mode 100644 index 00000000..78514569 --- /dev/null +++ b/plugins/puntmgr/rest.go @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 PANTHEON.tech +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package puntmgr + +// import ( +// "net/http" + +// "github.com/unrolled/render" +// "go.ligato.io/cn-infra/v2/rpc/rest" +// ) + +// func (p *Plugin) registerHandlers(handlers rest.HTTPHandlers) { +// if handlers == nil { +// p.Log.Debug("No http handler provided, skipping registration of REST handlers") +// return +// } +// if p.cnfMode == pb.CnfMode_STONEWORK { +// handlers.RegisterHTTPHandler("", ) +// } +// } + +// func (p *Plugin) \ No newline at end of file From d2bfbf4476b8d5378589ec1ed0fd7dd7f395ab58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Moti=C4=8D=C3=A1k?= Date: Thu, 6 Jul 2023 21:33:44 +0200 Subject: [PATCH 2/5] Remove unused file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Peter Motičák --- plugins/puntmgr/rest.go | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 plugins/puntmgr/rest.go diff --git a/plugins/puntmgr/rest.go b/plugins/puntmgr/rest.go deleted file mode 100644 index 78514569..00000000 --- a/plugins/puntmgr/rest.go +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -// Copyright 2023 PANTHEON.tech -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package puntmgr - -// import ( -// "net/http" - -// "github.com/unrolled/render" -// "go.ligato.io/cn-infra/v2/rpc/rest" -// ) - -// func (p *Plugin) registerHandlers(handlers rest.HTTPHandlers) { -// if handlers == nil { -// p.Log.Debug("No http handler provided, skipping registration of REST handlers") -// return -// } -// if p.cnfMode == pb.CnfMode_STONEWORK { -// handlers.RegisterHTTPHandler("", ) -// } -// } - -// func (p *Plugin) \ No newline at end of file From 0b7432e292dcc77ced164c6bd80bc577cea1d3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Moti=C4=8D=C3=A1k?= Date: Mon, 10 Jul 2023 15:54:37 +0200 Subject: [PATCH 3/5] Add additional flags to swctl status command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Peter Motičák --- client/client.go | 12 ++---- client/component.go | 22 ++--------- cmd/swctl/cli.go | 2 - cmd/swctl/cmd_status.go | 84 ++++++++++++++++++++++------------------- docs/SWCTL.md | 4 +- plugins/cnfreg/rest.go | 6 +-- 6 files changed, 58 insertions(+), 72 deletions(-) diff --git a/client/client.go b/client/client.go index 00cb8c06..f468bda2 100644 --- a/client/client.go +++ b/client/client.go @@ -26,10 +26,8 @@ import ( "time" docker "github.com/fsouza/go-dockerclient" - "github.com/sirupsen/logrus" vppagent "go.ligato.io/vpp-agent/v3/cmd/agentctl/client" "go.ligato.io/vpp-agent/v3/cmd/agentctl/client/tlsconfig" - linux_nsplugin "go.ligato.io/vpp-agent/v3/plugins/linux/nsplugin" "go.pantheon.tech/stonework/plugins/cnfreg" ) @@ -37,7 +35,6 @@ import ( const ( DefaultHost = "127.0.0.1" DefaultHTTPClientTimeout = 60 * time.Second - DefaultPortGRPC = 9991 DefaultPortHTTP = 9191 DockerComposeServiceLabel = "com.docker.compose.service" ) @@ -75,7 +72,6 @@ type API interface { // Client implements API interface. type Client struct { - nsPlugin *linux_nsplugin.NsPlugin dockerClient *docker.Client httpClient *http.Client host string @@ -101,7 +97,9 @@ func NewClient(opts ...Option) (*Client, error) { return nil, err } for _, o := range opts { - o(c) + if err = o(c); err != nil { + return nil, err + } } return c, nil @@ -172,9 +170,7 @@ func (c *Client) GetComponents() ([]Component, error) { return nil, err } containers = append(containers, c) - logrus.Warnf("[WLL] container name: %s, PID: %d, ENV: %+v", c.Name, c.State.Pid, c.Config.Env) } - logrus.Warnf("[WLL] number of containers: &d", len(containers)) cnfInfos := make(map[string]cnfreg.Info) for _, info := range infos { @@ -201,7 +197,7 @@ func (c *Client) GetComponents() ([]Component, error) { compo := &component{ agentclient: client, Name: info.MsLabel, - Mode: cnfModeToCompoMode(info.Mode), + Mode: cnfModeToCompoMode(info.CnfMode), Info: &info, } components = append(components, compo) diff --git a/client/component.go b/client/component.go index 7cc24816..b902c823 100644 --- a/client/component.go +++ b/client/component.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "go.ligato.io/cn-infra/v2/health/probe" "go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types" "go.ligato.io/vpp-agent/v3/cmd/agentctl/client" @@ -19,17 +18,16 @@ const ( // Foreign means the component is not managed by StoneWork ComponentForeign ComponentMode = iota - // Stonework means the component is a StoneWork instance - ComponentStonework - // StoneworkModule means the component is a StoneWork module managed by StoneWork ComponentStoneworkModule + + // Stonework means the component is a StoneWork instance + ComponentStonework ) // Component is a component of StoneWork. It can be StoneWork instance itself, // a CNF connected to it or other Ligato service in connected to StoneWork. type Component interface { - // Client() *client.Client GetName() string GetMode() ComponentMode GetInfo() *cnfreg.Info @@ -79,20 +77,6 @@ func (c *component) SchedulerValues() ([]*kvscheduler.BaseValueStatus, error) { return values, nil } -func (c *component) Readiness() (*probe.ExposedStatus, error) { - if c.Mode == ComponentForeign { - return nil, fmt.Errorf("cannot get readiness of component %s, this component in not managed by StoneWork", c.Name) - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - status, err := c.agentclient.Status(ctx) - if err != nil { - return nil, err - } - return status, nil -} - func cnfModeToCompoMode(cm cnfregpb.CnfMode) ComponentMode { switch cm { case cnfregpb.CnfMode_STANDALONE: diff --git a/cmd/swctl/cli.go b/cmd/swctl/cli.go index 45d4b1d4..13d466f9 100644 --- a/cmd/swctl/cli.go +++ b/cmd/swctl/cli.go @@ -10,8 +10,6 @@ import ( "github.com/moby/term" "github.com/sirupsen/logrus" - // agentcli "go.ligato.io/vpp-agent/v3/cmd/agentctl/cli" - "go.pantheon.tech/stonework/client" ) diff --git a/cmd/swctl/cmd_status.go b/cmd/swctl/cmd_status.go index 991b05db..5075507f 100644 --- a/cmd/swctl/cmd_status.go +++ b/cmd/swctl/cmd_status.go @@ -3,6 +3,7 @@ package main import ( "fmt" "io" + "os/exec" "strconv" "strings" "sync" @@ -10,7 +11,7 @@ import ( "github.com/gookit/color" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" - "golang.org/x/exp/constraints" + "github.com/spf13/pflag" "golang.org/x/exp/slices" "go.ligato.io/vpp-agent/v3/proto/ligato/kvscheduler" @@ -27,15 +28,23 @@ import ( const statusExample = ` # Show status for all components $ swctl status + + # Show interface status of StoneWork VPP instance + $ swctl status --show-interfaces ` -type StatusCmdOptions struct { - Args []string - Format string +type StatusOptions struct { + Format string + ShowInterfaces bool +} + +func (opts *StatusOptions) InstallFlags(flagset *pflag.FlagSet) { + flagset.StringVar(&opts.Format, "format", "", "Format for the output (yaml, json, go template)") + flagset.BoolVar(&opts.ShowInterfaces, "show-interfaces", false, "Show interface status of StoneWork VPP instance") } func NewStatusCmd(cli Cli) *cobra.Command { - var opts StatusCmdOptions + var opts StatusOptions cmd := &cobra.Command{ Use: "status [flags]", Short: "Show status of StoneWork components", @@ -45,10 +54,10 @@ func NewStatusCmd(cli Cli) *cobra.Command { UnknownFlags: true, }, RunE: func(cmd *cobra.Command, args []string) error { - opts.Args = args return runStatusCmd(cli, opts) }, } + opts.InstallFlags(cmd.PersistentFlags()) return cmd } @@ -57,7 +66,20 @@ type statusInfo struct { ConfigCounts configCounts } -func runStatusCmd(cli Cli, opts StatusCmdOptions) error { +func runStatusCmd(cli Cli, opts StatusOptions) error { + if opts.ShowInterfaces { + cmd := fmt.Sprintf("vpp-probe --env=%s --query label=%s=stonework discover", defaultVppProbeEnv, client.DockerComposeServiceLabel) + formatArg := fmt.Sprintf("--format=%s", opts.Format) + out, err := cli.Exec(cmd, []string{formatArg}) + if err != nil { + if ee, ok := err.(*exec.ExitError); ok { + return fmt.Errorf("%v: %s", ee.String(), ee.Stderr) + } + } + fmt.Fprintln(cli.Out(), out) + return nil + } + resp, err := cli.Client().GetComponents() if err != nil { return err @@ -71,11 +93,6 @@ func runStatusCmd(cli Cli, opts StatusCmdOptions) error { var wg sync.WaitGroup infoCh := make(chan infoWithErr) - type fetched struct { - values []*kvscheduler.BaseValueStatus - err error - } - for _, compo := range resp { wg.Add(1) go func(compo client.Component) { @@ -108,11 +125,14 @@ func runStatusCmd(cli Cli, opts StatusCmdOptions) error { } infos = append(infos, i.statusInfo) } - slices.SortFunc(infos, cmpInfos) - // if err := formatAsTemplate(cli.Out(), "json", resp); err != nil { - // return err - // } - printStatusTable(cli.Out(), infos) + slices.SortFunc(infos, cmpStatus) + if opts.Format == "" { + printStatusTable(cli.Out(), infos) + } else { + if err := formatAsTemplate(cli.Out(), opts.Format, infos); err != nil { + return err + } + } return nil } @@ -143,24 +163,12 @@ func countConfig(baseVals []*kvscheduler.BaseValueStatus) configCounts { return res } -type comparable interface { - constraints.Integer | ~string -} - -func less[T comparable](a, b T) bool { - if a > b { - return true +func cmpStatus(a, b statusInfo) bool { + greater := a.GetMode() > b.GetMode() + if !greater && a.GetMode() == b.GetMode() { + greater = a.GetName() > b.GetName() } - return false -} - -func cmpInfos(a, b statusInfo) bool { - res := less(a.GetMode(), b.GetMode()) - bLessA := less(b.GetMode(), a.GetMode()) - if !(res || bLessA) { - res = less(a.GetName(), b.GetName()) - } - return res + return greater } func printStatusTable(out io.Writer, infos []statusInfo) { @@ -193,8 +201,8 @@ func printStatusTable(out io.Writer, infos []statusInfo) { table.Rich(row, clrs) continue } - config := info.ConfigCounts.String() - configColor := info.ConfigCounts.Color() + config := info.ConfigCounts.string() + configColor := info.ConfigCounts.color() compoInfo := info.GetInfo() grpcState := compoInfo.GRPCConnState.String() var statusClr int @@ -238,7 +246,7 @@ type configCounts struct { Unimplemented int } -func (c configCounts) String() string { +func (c configCounts) string() string { var fields []string if c.Ok != 0 { fields = append(fields, fmt.Sprintf("%d OK", c.Ok)) @@ -265,7 +273,7 @@ func (c configCounts) String() string { return strings.Join(fields, ", ") } -func (c configCounts) Color() int { +func (c configCounts) color() int { if c.Err > 0 { return tablewriter.FgHiRedColor } diff --git a/docs/SWCTL.md b/docs/SWCTL.md index 28507e02..90346711 100644 --- a/docs/SWCTL.md +++ b/docs/SWCTL.md @@ -228,14 +228,14 @@ swctl config history #### Status -To display the status of StoneWork components and their interfaces, run: +To display the status of StoneWork components, run: ```bash swctl status ``` > **Note** -> The `status` command is a simple wrapper for `vpp-probe discover`. +> When used with `--show-interfaces` flag the `status` command calls `vpp-probe discover`. #### Trace diff --git a/plugins/cnfreg/rest.go b/plugins/cnfreg/rest.go index e6729340..b25e42d8 100644 --- a/plugins/cnfreg/rest.go +++ b/plugins/cnfreg/rest.go @@ -30,7 +30,7 @@ import ( type Info struct { PID int MsLabel string - Mode pb.CnfMode + CnfMode pb.CnfMode IPAddr string GRPCPort int HTTPPort int @@ -54,7 +54,7 @@ func (p *Plugin) statusHandler(formatter *render.Render) http.HandlerFunc { swInfo := &Info{ PID: os.Getpid(), MsLabel: p.ServiceLabel.GetAgentLabel(), - Mode: pb.CnfMode_STONEWORK, + CnfMode: pb.CnfMode_STONEWORK, IPAddr: p.ipAddress.String(), GRPCPort: p.GRPCPlugin.GetPort(), HTTPPort: p.HTTPPlugin.GetPort(), @@ -66,7 +66,7 @@ func (p *Plugin) statusHandler(formatter *render.Render) http.HandlerFunc { swModInfo := &Info{ PID: swMod.pid, MsLabel: swMod.cnfMsLabel, - Mode: pb.CnfMode_STONEWORK_MODULE, + CnfMode: pb.CnfMode_STONEWORK_MODULE, IPAddr: swMod.ipAddress, GRPCPort: swMod.grpcPort, HTTPPort: swMod.httpPort, From faefa7213f1cbec03486fc6fa368313b2df9bf78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Moti=C4=8D=C3=A1k?= Date: Mon, 10 Jul 2023 16:41:19 +0200 Subject: [PATCH 4/5] Cleanup code, add license headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Peter Motičák --- client/client.go | 4 ++-- client/component.go | 18 +++++++++++++++++- client/http.go | 16 ++++++++++++++++ cmd/swctl/cli.go | 24 +++--------------------- cmd/swctl/cmd_status.go | 24 +++++++++++++++++------- plugins/cnfreg/plugin.go | 15 +++++++-------- plugins/cnfreg/rest.go | 2 +- 7 files changed, 63 insertions(+), 40 deletions(-) diff --git a/client/client.go b/client/client.go index f468bda2..c2d2496a 100644 --- a/client/client.go +++ b/client/client.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2021 PANTHEON.tech +// Copyright 2023 PANTHEON.tech // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -221,4 +221,4 @@ func containsPrefix(strs []string, prefix string) (string, bool) { } } return "", false -} \ No newline at end of file +} diff --git a/client/component.go b/client/component.go index b902c823..4232e837 100644 --- a/client/component.go +++ b/client/component.go @@ -1,3 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 PANTHEON.tech +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package client import ( @@ -88,4 +104,4 @@ func cnfModeToCompoMode(cm cnfregpb.CnfMode) ComponentMode { default: return ComponentForeign } -} \ No newline at end of file +} diff --git a/client/http.go b/client/http.go index 45d8f7f2..c2d949e5 100644 --- a/client/http.go +++ b/client/http.go @@ -1,3 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 PANTHEON.tech +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package client import ( diff --git a/cmd/swctl/cli.go b/cmd/swctl/cli.go index 13d466f9..8ef5088f 100644 --- a/cmd/swctl/cli.go +++ b/cmd/swctl/cli.go @@ -64,8 +64,7 @@ func NewCli(opt ...CliOption) (*CLI, error) { } func (cli *CLI) Initialize(opts Options) (err error) { - // clientCfg, err := agentcli.MakeConfig() - cli.client, err = initClient() + cli.client, err = initClient(opts) if err != nil { return fmt.Errorf("init error: %w", err) } @@ -87,25 +86,8 @@ func (cli *CLI) Initialize(opts Options) (err error) { return nil } -func initClient() (*client.Client, error) { - // cfg, err := agentcli.MakeConfig() - // if err != nil { - // return nil, err - // } - opts := []client.Option{ - // client.WithHost(cfg.Host), - // client.WithGRPCPort(uint16(cfg.GRPCPort)), - // client.WithHTTPPort(uint16(cfg.HTTPPort)), - } - // if cfg.ShouldUseSecureGRPC() { - // opts = append(opts, client.WithGRPCTLS( - // cfg.GRPCSecure.CertFile, - // cfg.GRPCSecure.KeyFile, - // cfg.GRPCSecure.CAFile, - // cfg.GRPCSecure.SkipVerify, - // )) - // } - c, err := client.NewClient(opts...) +func initClient(opts Options) (*client.Client, error) { + c, err := client.NewClient() if err != nil { return nil, err } diff --git a/cmd/swctl/cmd_status.go b/cmd/swctl/cmd_status.go index 5075507f..6fb2f407 100644 --- a/cmd/swctl/cmd_status.go +++ b/cmd/swctl/cmd_status.go @@ -1,3 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 PANTHEON.tech +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package main import ( @@ -19,12 +35,6 @@ import ( "go.pantheon.tech/stonework/client" ) -// TODO: improve status overview, show status of components (CNFs) -// - instead of using raw output from vpp-probe, retrieve the important info -// about the running/deployed components of StoneWork and show those by default -// - optionally allow user to set more details which shows the more detailed output -// similar to vpp-probe discover - const statusExample = ` # Show status for all components $ swctl status @@ -290,4 +300,4 @@ func (c configCounts) color() int { return tablewriter.FgGreenColor } return 0 -} \ No newline at end of file +} diff --git a/plugins/cnfreg/plugin.go b/plugins/cnfreg/plugin.go index 997df5a4..9d09c448 100644 --- a/plugins/cnfreg/plugin.go +++ b/plugins/cnfreg/plugin.go @@ -174,10 +174,9 @@ type swModule struct { grpcPort int httpPort int grpcConn *grpc.ClientConn - // grpcConn grpc.ClientConnInterface - cnfClient pb.CnfDiscoveryClient - cfgClient client.GenericClient - cnfModels []cnfModel + cnfClient pb.CnfDiscoveryClient + cfgClient client.GenericClient + cnfModels []cnfModel } // Attributes specific to StoneWork Module (i.e. not used by standalone CNF or StoneWork itself). @@ -244,10 +243,6 @@ func (p *Plugin) Init() (err error) { p.Log.Infof("Discovered management IP address: %v", p.ipAddress) } - grpcServer := p.GRPCPlugin.GetServer() - if grpcServer == nil { - return errors.New("gRPC server is not initialized") - } switch p.cnfMode { case pb.CnfMode_STONEWORK_MODULE: // inject gRPC and HTTP ports to use by SW-Module @@ -257,6 +252,10 @@ func (p *Plugin) Init() (err error) { } // serve CnfDiscovery methods + grpcServer := p.GRPCPlugin.GetServer() + if grpcServer == nil { + return errors.New("gRPC server is not initialized") + } pb.RegisterCnfDiscoveryServer(grpcServer, p) case pb.CnfMode_STONEWORK: diff --git a/plugins/cnfreg/rest.go b/plugins/cnfreg/rest.go index b25e42d8..81d5aa58 100644 --- a/plugins/cnfreg/rest.go +++ b/plugins/cnfreg/rest.go @@ -78,4 +78,4 @@ func (p *Plugin) statusHandler(formatter *render.Render) http.HandlerFunc { p.Log.Error(err) } } -} \ No newline at end of file +} From a9227b98e54ed1951237fe660521837c3e0f15a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Moti=C4=8D=C3=A1k?= Date: Wed, 19 Jul 2023 20:25:12 +0200 Subject: [PATCH 5/5] Address code review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also improve StoneWork service management IP address detection for swctl Signed-off-by: Peter Motičák --- client/client.go | 93 ++++++++++++++++++--------- client/component.go | 100 ++++++++++++++++++++++++++--- cmd/swctl/cmd_status.go | 136 ++++++++++------------------------------ 3 files changed, 188 insertions(+), 141 deletions(-) diff --git a/client/client.go b/client/client.go index c2d2496a..62e6d429 100644 --- a/client/client.go +++ b/client/client.go @@ -26,6 +26,7 @@ import ( "time" docker "github.com/fsouza/go-dockerclient" + "github.com/sirupsen/logrus" vppagent "go.ligato.io/vpp-agent/v3/cmd/agentctl/client" "go.ligato.io/vpp-agent/v3/cmd/agentctl/client/tlsconfig" @@ -33,7 +34,7 @@ import ( ) const ( - DefaultHost = "127.0.0.1" + FallbackHost = "127.0.0.1" DefaultHTTPClientTimeout = 60 * time.Second DefaultPortHTTP = 9191 DockerComposeServiceLabel = "com.docker.compose.service" @@ -42,13 +43,6 @@ const ( // Option is a function that customizes a Client. type Option func(*Client) error -func WithHost(h string) Option { - return func(c *Client) error { - c.host = h - return nil - } -} - func WithHTTPPort(p uint16) Option { return func(c *Client) error { c.httpPort = p @@ -86,21 +80,51 @@ type Client struct { // customized by options. func NewClient(opts ...Option) (*Client, error) { c := &Client{ - host: DefaultHost, scheme: "http", protocol: "tcp", httpPort: DefaultPortHTTP, } var err error + c.dockerClient, err = docker.NewClientFromEnv() if err != nil { return nil, err } + + containers, err := c.dockerClient.ListContainers(docker.ListContainersOptions{}) + if err != nil { + return nil, err + } + + // find IP address of the StoneWork service + for _, container := range containers { + if container.Labels[DockerComposeServiceLabel] != "stonework" { + continue + } + cont, err := c.dockerClient.InspectContainerWithOptions(docker.InspectContainerOptions{ID: container.ID}) + if err != nil { + return nil, err + } + for _, nw := range cont.NetworkSettings.Networks { + if nw.IPAddress != "" { + c.host = nw.IPAddress + break + } + } + break + } + for _, o := range opts { if err = o(c); err != nil { return nil, err } } + if c.host == "" { + logrus.Warnf("could not find StoneWork service management IP address falling back to: %s", FallbackHost) + c.host = FallbackHost + } else { + logrus.Debugf("found StoneWork service management IP address: %s", c.host) + } return c, nil } @@ -178,36 +202,49 @@ func (c *Client) GetComponents() ([]Component, error) { } var components []Component - var foreignContainers []*docker.Container for _, container := range containers { + + metadata := make(map[string]string) + metadata["containerID"] = container.ID + metadata["containerName"] = container.Name + metadata["containerServiceName"] = container.Config.Labels[DockerComposeServiceLabel] + metadata["dockerImage"] = container.Config.Image + if container.NetworkSettings.IPAddress != "" { + metadata["containerIPAddress"] = container.NetworkSettings.IPAddress + } else { + for _, nw := range container.NetworkSettings.Networks { + if nw.IPAddress != "" { + metadata["containerIPAddress"] = nw.IPAddress + break + } + } + } + + logrus.Tracef("found metadata for container: %s, data: %+v", container.Name, metadata) + + compo := &component{Metadata: metadata} after, found := containsPrefix(container.Config.Env, "MICROSERVICE_LABEL=") if !found { - foreignContainers = append(foreignContainers, container) + compo.Name = container.Config.Labels[DockerComposeServiceLabel] + compo.Mode = ComponentAuxiliary + components = append(components, compo) continue } info, ok := cnfInfos[after] - if !ok { - foreignContainers = append(foreignContainers, container) - continue + if ok { + compo.Name = info.MsLabel + compo.Info = &info + compo.Mode = cnfModeToCompoMode(info.CnfMode) + } else { + compo.Name = container.Config.Labels[DockerComposeServiceLabel] + compo.Mode = ComponentStandalone } + client, err := vppagent.NewClientWithOpts(vppagent.WithHost(info.IPAddr), vppagent.WithHTTPPort(info.HTTPPort)) if err != nil { return components, err } - compo := &component{ - agentclient: client, - Name: info.MsLabel, - Mode: cnfModeToCompoMode(info.CnfMode), - Info: &info, - } - components = append(components, compo) - } - - for _, fcontainer := range foreignContainers { - compo := &component{ - Name: fcontainer.Config.Labels[DockerComposeServiceLabel], - Mode: ComponentForeign, - } + compo.agentclient = client components = append(components, compo) } return components, nil diff --git a/client/component.go b/client/component.go index 4232e837..16858d8a 100644 --- a/client/component.go +++ b/client/component.go @@ -19,6 +19,7 @@ package client import ( "context" "fmt" + "strings" "go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types" "go.ligato.io/vpp-agent/v3/cmd/agentctl/client" @@ -31,10 +32,15 @@ import ( type ComponentMode int32 const ( - // Foreign means the component is not managed by StoneWork - ComponentForeign ComponentMode = iota + ComponentUnknown ComponentMode = iota - // StoneworkModule means the component is a StoneWork module managed by StoneWork + // Auxiliary means the component is not a CNF and is not managed by StoneWork + ComponentAuxiliary + + // Standalone means the component is a standalone CNF + ComponentStandalone + + // ComponentStonework means the component is a StoneWork module managed by StoneWork ComponentStoneworkModule // Stonework means the component is a StoneWork instance @@ -48,7 +54,7 @@ type Component interface { GetMode() ComponentMode GetInfo() *cnfreg.Info GetMetadata() map[string]string - SchedulerValues() ([]*kvscheduler.BaseValueStatus, error) + ConfigStatus() (*ConfigCounts, error) } type component struct { @@ -79,8 +85,8 @@ func (c *component) GetMetadata() map[string]string { return c.Metadata } -func (c *component) SchedulerValues() ([]*kvscheduler.BaseValueStatus, error) { - if c.Mode == ComponentForeign { +func (c *component) ConfigStatus() (*ConfigCounts, error) { + if c.Mode == ComponentAuxiliary || c.Mode == ComponentUnknown { return nil, fmt.Errorf("cannot get scheduler values of component %s, this component in not managed by StoneWork", c.Name) } ctx, cancel := context.WithCancel(context.Background()) @@ -90,18 +96,94 @@ func (c *component) SchedulerValues() ([]*kvscheduler.BaseValueStatus, error) { if err != nil { return nil, err } - return values, nil + + var allVals []*kvscheduler.ValueStatus + for _, baseVal := range values { + allVals = append(allVals, baseVal.Value) + allVals = append(allVals, baseVal.DerivedValues...) + } + + var res ConfigCounts + for _, val := range allVals { + switch val.State { + case kvscheduler.ValueState_INVALID, kvscheduler.ValueState_FAILED: + res.Err++ + case kvscheduler.ValueState_MISSING: + res.Missing++ + case kvscheduler.ValueState_PENDING: + res.Pending++ + case kvscheduler.ValueState_RETRYING: + res.Retrying++ + case kvscheduler.ValueState_UNIMPLEMENTED: + res.Unimplemented++ + case kvscheduler.ValueState_CONFIGURED, kvscheduler.ValueState_DISCOVERED, kvscheduler.ValueState_OBTAINED, kvscheduler.ValueState_REMOVED, kvscheduler.ValueState_NONEXISTENT: + res.Ok++ + } + } + + return &res, nil +} + +type ConfigCounts struct { + Ok int + Err int + Missing int + Pending int + Retrying int + Unimplemented int +} + +func (cc ConfigCounts) String() string { + var fields []string + if cc.Ok != 0 { + fields = append(fields, fmt.Sprintf("%d OK", cc.Ok)) + } + if cc.Err != 0 { + errStr := fmt.Sprintf("%d errors", cc.Ok) + if cc.Err == 1 { + errStr = errStr[:len(errStr)-1] + } + fields = append(fields, errStr) + } + if cc.Missing != 0 { + fields = append(fields, fmt.Sprintf("%d missing", cc.Missing)) + } + if cc.Pending != 0 { + fields = append(fields, fmt.Sprintf("%d pending", cc.Pending)) + } + if cc.Retrying != 0 { + fields = append(fields, fmt.Sprintf("%d retrying", cc.Retrying)) + } + if cc.Unimplemented != 0 { + fields = append(fields, fmt.Sprintf("%d unimplemented", cc.Unimplemented)) + } + return strings.Join(fields, ", ") +} + +func (c ComponentMode) String() string { + switch c { + case ComponentAuxiliary: + return "auxiliary" + case ComponentStandalone: + return "standalone CNF" + case ComponentStonework: + return "StoneWork" + case ComponentStoneworkModule: + return "StoneWork module" + default: + return "unknown" + } } func cnfModeToCompoMode(cm cnfregpb.CnfMode) ComponentMode { switch cm { case cnfregpb.CnfMode_STANDALONE: - return ComponentForeign + return ComponentStandalone case cnfregpb.CnfMode_STONEWORK_MODULE: return ComponentStoneworkModule case cnfregpb.CnfMode_STONEWORK: return ComponentStonework default: - return ComponentForeign + return ComponentUnknown } } diff --git a/cmd/swctl/cmd_status.go b/cmd/swctl/cmd_status.go index 6fb2f407..bcedc70e 100644 --- a/cmd/swctl/cmd_status.go +++ b/cmd/swctl/cmd_status.go @@ -26,12 +26,11 @@ import ( "github.com/gookit/color" "github.com/olekukonko/tablewriter" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" "golang.org/x/exp/slices" - "go.ligato.io/vpp-agent/v3/proto/ligato/kvscheduler" - "go.pantheon.tech/stonework/client" ) @@ -73,28 +72,33 @@ func NewStatusCmd(cli Cli) *cobra.Command { type statusInfo struct { client.Component - ConfigCounts configCounts + ConfigCounts *client.ConfigCounts } func runStatusCmd(cli Cli, opts StatusOptions) error { + resp, err := cli.Client().GetComponents() + if err != nil { + return err + } + if opts.ShowInterfaces { - cmd := fmt.Sprintf("vpp-probe --env=%s --query label=%s=stonework discover", defaultVppProbeEnv, client.DockerComposeServiceLabel) - formatArg := fmt.Sprintf("--format=%s", opts.Format) - out, err := cli.Exec(cmd, []string{formatArg}) - if err != nil { - if ee, ok := err.(*exec.ExitError); ok { - return fmt.Errorf("%v: %s", ee.String(), ee.Stderr) + for _, compo := range resp { + if sn, ok := compo.GetMetadata()["containerServiceName"]; ok { + cmd := fmt.Sprintf("vpp-probe --env=%s --query label=%s=%s discover", defaultVppProbeEnv, client.DockerComposeServiceLabel, sn) + formatArg := fmt.Sprintf("--format=%s", opts.Format) + out, err := cli.Exec(cmd, []string{formatArg}) + if err != nil { + if ee, ok := err.(*exec.ExitError); ok { + logrus.Tracef("vpp-probe discover failed for service %s with error: %v: %s", sn, ee.String(), ee.Stderr) + continue + } + } + fmt.Fprintln(cli.Out(), out) } } - fmt.Fprintln(cli.Out(), out) return nil } - resp, err := cli.Client().GetComponents() - if err != nil { - return err - } - type infoWithErr struct { statusInfo error @@ -107,13 +111,12 @@ func runStatusCmd(cli Cli, opts StatusOptions) error { wg.Add(1) go func(compo client.Component) { defer wg.Done() - var counts configCounts - if compo.GetMode() != client.ComponentForeign { - vals, err := compo.SchedulerValues() + var counts *client.ConfigCounts + if compo.GetMode() != client.ComponentAuxiliary { + counts, err = compo.ConfigStatus() if err != nil { infoCh <- infoWithErr{error: err} } - counts = countConfig(vals) } infoCh <- infoWithErr{ statusInfo: statusInfo{ @@ -146,33 +149,6 @@ func runStatusCmd(cli Cli, opts StatusOptions) error { return nil } -func countConfig(baseVals []*kvscheduler.BaseValueStatus) configCounts { - var allVals []*kvscheduler.ValueStatus - for _, baseVal := range baseVals { - allVals = append(allVals, baseVal.Value) - allVals = append(allVals, baseVal.DerivedValues...) - } - - var res configCounts - for _, val := range allVals { - switch val.State { - case kvscheduler.ValueState_INVALID, kvscheduler.ValueState_FAILED: - res.Err++ - case kvscheduler.ValueState_MISSING: - res.Missing++ - case kvscheduler.ValueState_PENDING: - res.Pending++ - case kvscheduler.ValueState_RETRYING: - res.Retrying++ - case kvscheduler.ValueState_UNIMPLEMENTED: - res.Unimplemented++ - case kvscheduler.ValueState_CONFIGURED, kvscheduler.ValueState_DISCOVERED, kvscheduler.ValueState_OBTAINED, kvscheduler.ValueState_REMOVED, kvscheduler.ValueState_NONEXISTENT: - res.Ok++ - } - } - return res -} - func cmpStatus(a, b statusInfo) bool { greater := a.GetMode() > b.GetMode() if !greater && a.GetMode() == b.GetMode() { @@ -200,9 +176,9 @@ func printStatusTable(out io.Writer, infos []statusInfo) { table.SetBorder(false) table.SetTablePadding("\t") for _, info := range infos { - row := []string{info.GetName(), compoModeString(info.GetMode())} + row := []string{info.GetName(), info.GetMode().String()} var clrs []tablewriter.Colors - if info.GetMode() == client.ComponentForeign { + if info.GetMode() == client.ComponentAuxiliary { clrs = []tablewriter.Colors{{}, {}} for i := range header[2:] { clrs = append(clrs, []int{tablewriter.FgHiBlackColor}) @@ -211,8 +187,8 @@ func printStatusTable(out io.Writer, infos []statusInfo) { table.Rich(row, clrs) continue } - config := info.ConfigCounts.string() - configColor := info.ConfigCounts.color() + config := info.ConfigCounts.String() + configColor := configColor(info.ConfigCounts) compoInfo := info.GetInfo() grpcState := compoInfo.GRPCConnState.String() var statusClr int @@ -235,68 +211,20 @@ func printStatusTable(out io.Writer, infos []statusInfo) { table.Render() } -func compoModeString(c client.ComponentMode) string { - switch c { - case client.ComponentForeign: - return "foreign" - case client.ComponentStonework: - return "StoneWork" - case client.ComponentStoneworkModule: - return "StoneWork module" - } - return "unknown" -} - -type configCounts struct { - Ok int - Err int - Missing int - Pending int - Retrying int - Unimplemented int -} - -func (c configCounts) string() string { - var fields []string - if c.Ok != 0 { - fields = append(fields, fmt.Sprintf("%d OK", c.Ok)) - } - if c.Err != 0 { - errStr := fmt.Sprintf("%d errors", c.Ok) - if c.Err == 1 { - errStr = errStr[:len(errStr)-1] - } - fields = append(fields, errStr) - } - if c.Missing != 0 { - fields = append(fields, fmt.Sprintf("%d missing", c.Missing)) - } - if c.Pending != 0 { - fields = append(fields, fmt.Sprintf("%d pending", c.Pending)) - } - if c.Retrying != 0 { - fields = append(fields, fmt.Sprintf("%d retrying", c.Retrying)) - } - if c.Unimplemented != 0 { - fields = append(fields, fmt.Sprintf("%d unimplemented", c.Unimplemented)) - } - return strings.Join(fields, ", ") -} - -func (c configCounts) color() int { - if c.Err > 0 { +func configColor(cc *client.ConfigCounts) int { + if cc.Err > 0 { return tablewriter.FgHiRedColor } - if c.Retrying > 0 || c.Pending > 0 { + if cc.Retrying > 0 || cc.Pending > 0 { return tablewriter.FgYellowColor } - if c.Unimplemented > 0 { + if cc.Unimplemented > 0 { return tablewriter.FgMagentaColor } - if c.Missing > 0 { + if cc.Missing > 0 { return tablewriter.FgHiYellowColor } - if c.Ok > 0 { + if cc.Ok > 0 { return tablewriter.FgGreenColor } return 0