diff --git a/.changelog/4790.txt b/.changelog/4790.txt new file mode 100644 index 0000000000..f2e3e59b78 --- /dev/null +++ b/.changelog/4790.txt @@ -0,0 +1,3 @@ +```release-note:feature +cli: Updated the status command to output the consul client & deployments status as well along with existing ones. +``` \ No newline at end of file diff --git a/cli/cmd/status/status.go b/cli/cmd/status/status.go index ebe60528f2..40cf663dce 100644 --- a/cli/cmd/status/status.go +++ b/cli/cmd/status/status.go @@ -7,9 +7,12 @@ import ( "errors" "fmt" "strconv" + "strings" "sync" + "time" "github.com/posener/complete" + "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/release" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -17,12 +20,13 @@ import ( "github.com/hashicorp/consul-k8s/cli/common/flag" "github.com/hashicorp/consul-k8s/cli/common/terminal" "github.com/hashicorp/consul-k8s/cli/helm" - "helm.sh/helm/v3/pkg/action" helmCLI "helm.sh/helm/v3/pkg/cli" "k8s.io/client-go/kubernetes" "sigs.k8s.io/yaml" ) +var tableHeaderForConsulComponents = []string{"NAME", "READY", "AGE", "CONTAINERS", "IMAGES"} + const ( flagNameKubeConfig = "kubeconfig" flagNameKubeContext = "context" @@ -122,11 +126,10 @@ func (c *Command) Run(args []string) int { return 1 } - if err := c.checkConsulServers(namespace); err != nil { - c.UI.Output("Unable to check Kubernetes cluster for Consul servers: %v", err) + err = c.checkConsulComponentsStatus(namespace) + if err != nil { return 1 } - return 0 } @@ -217,24 +220,127 @@ func validEvent(events []release.HookEvent) bool { return false } -// checkConsulServers prints the status of Consul servers if they -// are expected to be found in the Kubernetes cluster. It does not check for -// server status if they are not running within the Kubernetes cluster. -func (c *Command) checkConsulServers(namespace string) error { +// checkConsulComponentsStatus fetch and prints the status of different consul components +// like Consul Clients, Consul Servers, and Consul Deployments, in the given namespace of the cluster. +func (c *Command) checkConsulComponentsStatus(namespace string) error { + var err error + var tbl *terminal.Table + tbl, err = c.getConsulClientsTable(namespace) + c.printComponentStatus(tbl, err, "Consul Clients") + tbl, err = c.getConsulServersTable(namespace) + c.printComponentStatus(tbl, err, "Consul Servers") + tbl, err = c.getConsulDeploymentsTable(namespace) + c.printComponentStatus(tbl, err, "Consul Deployments") + return err +} + +// printComponentStatus prints the status of a given component (Consul Clients, Consul Servers, or Consul Deployments). +func (c *Command) printComponentStatus(tbl *terminal.Table, err error, component string) { + c.UI.Output(fmt.Sprintf("%s status: ", component), terminal.WithHeaderStyle()) + if err != nil { + c.UI.Output("unable to list %s: %s", component, err, terminal.WithErrorStyle()) + } + if tbl != nil { + c.UI.Table(tbl) + } else { + c.UI.Output(fmt.Sprintf("No %s found in Kubernetes cluster.", component)) + } +} + +// getConsulClientsTable returns the table instance with the Consul Clients +// and their ready status (number of pods ready/desired) +func (c *Command) getConsulClientsTable(namespace string) (*terminal.Table, error) { + clients, err := c.kubernetes.AppsV1().DaemonSets(namespace).List(c.Ctx, metav1.ListOptions{LabelSelector: "app=consul,chart=consul-helm,component=client"}) + if err != nil { + return nil, err + } + var tbl *terminal.Table + if len(clients.Items) != 0 { + tbl = terminal.NewTable(tableHeaderForConsulComponents...) + for _, c := range clients.Items { + age := time.Since(c.CreationTimestamp.Time).Round(time.Minute) + readyStatus := fmt.Sprintf("%d/%d", c.Status.NumberReady, c.Status.DesiredNumberScheduled) + + var containers, images []string + for _, container := range c.Spec.Template.Spec.Containers { + containers = append(containers, fmt.Sprintf("%s", container.Name)) + images = append(images, fmt.Sprintf("%s", container.Image)) + } + imagesString := strings.Join(images, ", ") + containersString := strings.Join(containers, ", ") + if c.Status.NumberReady != c.Status.DesiredNumberScheduled { + colourCode := []string{terminal.Red, terminal.Red, "", "", ""} + tbl.AddRow([]string{c.Name, readyStatus, age.String(), containersString, imagesString}, colourCode) + } else { + tbl.AddRow([]string{c.Name, readyStatus, age.String(), containersString, imagesString}, []string{}) + } + } + } + return tbl, nil +} + +// getConsulServersTable returns the table instance with the Consul Servers +// and their ready status (number of pods ready/desired), +func (c *Command) getConsulServersTable(namespace string) (*terminal.Table, error) { servers, err := c.kubernetes.AppsV1().StatefulSets(namespace).List(c.Ctx, metav1.ListOptions{LabelSelector: "app=consul,chart=consul-helm,component=server"}) if err != nil { - return err + return nil, err } + var tbl *terminal.Table if len(servers.Items) != 0 { - desiredServers, readyServers := int(*servers.Items[0].Spec.Replicas), int(servers.Items[0].Status.ReadyReplicas) - if readyServers < desiredServers { - c.UI.Output("Consul servers healthy %d/%d", readyServers, desiredServers, terminal.WithErrorStyle()) - } else { - c.UI.Output("Consul servers healthy %d/%d", readyServers, desiredServers) + tbl = terminal.NewTable(tableHeaderForConsulComponents...) + for _, s := range servers.Items { + age := time.Since(s.CreationTimestamp.Time).Round(time.Minute) + readyStatus := fmt.Sprintf("%d/%d", s.Status.ReadyReplicas, *s.Spec.Replicas) + + var containers, images []string + for _, container := range s.Spec.Template.Spec.Containers { + containers = append(containers, fmt.Sprintf("%s", container.Name)) + images = append(images, fmt.Sprintf("%s", container.Image)) + } + imagesString := strings.Join(images, ", ") + containersString := strings.Join(containers, ", ") + if s.Status.ReadyReplicas != *s.Spec.Replicas { + colourCode := []string{terminal.Red, terminal.Red, "", "", ""} + tbl.AddRow([]string{s.Name, readyStatus, age.String(), containersString, imagesString}, colourCode) + } else { + tbl.AddRow([]string{s.Name, readyStatus, age.String(), containersString, imagesString}, []string{}) + } } } + return tbl, nil +} - return nil +// getConsulDeploymentsTable returns the table instance with the Consul Deployed Deployments +// and their ready status (number of pods ready/desired), +func (c *Command) getConsulDeploymentsTable(namespace string) (*terminal.Table, error) { + deployments, err := c.kubernetes.AppsV1().Deployments(namespace).List(c.Ctx, metav1.ListOptions{LabelSelector: "app=consul,chart=consul-helm"}) + if err != nil { + return nil, err + } + var tbl *terminal.Table + if len(deployments.Items) != 0 { + tbl = terminal.NewTable(tableHeaderForConsulComponents...) + for _, d := range deployments.Items { + age := time.Since(d.CreationTimestamp.Time).Round(time.Minute) + readyStatus := fmt.Sprintf("%d/%d", d.Status.ReadyReplicas, *d.Spec.Replicas) + + var containers, images []string + for _, container := range d.Spec.Template.Spec.Containers { + containers = append(containers, fmt.Sprintf("%s", container.Name)) + images = append(images, fmt.Sprintf("%s", container.Image)) + } + imagesString := strings.Join(images, ", ") + containersString := strings.Join(containers, ", ") + if d.Status.ReadyReplicas != *d.Spec.Replicas { + colourCode := []string{terminal.Red, terminal.Red, "", "", ""} + tbl.AddRow([]string{d.Name, readyStatus, age.String(), containersString, imagesString}, colourCode) + } else { + tbl.AddRow([]string{d.Name, readyStatus, age.String(), containersString, imagesString}, []string{}) + } + } + } + return tbl, nil } // setupKubeClient to use for non Helm SDK calls to the Kubernetes API The Helm SDK will use diff --git a/cli/cmd/status/status_test.go b/cli/cmd/status/status_test.go index 7984415c43..817548fdc5 100644 --- a/cli/cmd/status/status_test.go +++ b/cli/cmd/status/status_test.go @@ -48,17 +48,126 @@ func TestCheckConsulServers(t *testing.T) { c := getInitializedCommand(t, buf) c.kubernetes = fake.NewSimpleClientset() - // Deploy servers - err := createServers("consul-servers", namespace, int32(tc.desired), int32(tc.healthy), c.kubernetes) - require.NoError(t, err) + // Deploy servers if needed. + if tc.desired != 0 { + err := createServers("consul-server", namespace, int32(tc.desired), int32(tc.healthy), c.kubernetes) + require.NoError(t, err) + } // Verify that the correct server statuses are seen. - err = c.checkConsulServers(namespace) + tbl, err := c.getConsulServersTable(namespace) + require.NoError(t, err) + + if tc.desired > 0 { + require.NotNil(t, tbl) + expectedHeaders := tableHeaderForConsulComponents + assert.Equal(t, expectedHeaders, tbl.Headers) + + require.Len(t, tbl.Rows, 1) + serverRow := tbl.Rows[0] + + require.Equal(t, "consul-server", serverRow[0].Value) + require.Equal(t, fmt.Sprintf("%d/%d", tc.healthy, tc.desired), serverRow[1].Value) + if tc.desired != tc.healthy { + require.Equal(t, terminal.Red, serverRow[1].Color) + } + } else { + require.Nil(t, tbl) + } + buf.Reset() + }) + } +} +func TestCheckConsulClients(t *testing.T) { + namespace := "default" + cases := map[string]struct { + desired int + healthy int + }{ + "No clients": {0, 0}, + "3 clients expected, 1 healthy": {3, 1}, + "3 clients expected, 3 healthy": {3, 3}, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + buf := new(bytes.Buffer) + c := getInitializedCommand(t, buf) + c.kubernetes = fake.NewSimpleClientset() + + // Deploy clients if needed. + if tc.desired != 0 { + err := createClients("consul-client", namespace, int32(tc.desired), int32(tc.healthy), c.kubernetes) + require.NoError(t, err) + } + + // Verify that the correct clients statuses are seen. + tbl, err := c.getConsulClientsTable(namespace) require.NoError(t, err) - actual := buf.String() + if tc.desired > 0 { + require.NotNil(t, tbl) + expectedHeaders := tableHeaderForConsulComponents + assert.Equal(t, expectedHeaders, tbl.Headers) + + require.Len(t, tbl.Rows, 1) + serverRow := tbl.Rows[0] + + require.Equal(t, "consul-client", serverRow[0].Value) + require.Equal(t, fmt.Sprintf("%d/%d", tc.healthy, tc.desired), serverRow[1].Value) + if tc.desired != tc.healthy { + require.Equal(t, terminal.Red, serverRow[1].Color) + } + } else { + require.Nil(t, tbl) + } + buf.Reset() + }) + } + +} +func TestCheckConsulDeployments(t *testing.T) { + namespace := "default" + cases := map[string]struct { + desired int + healthy int + }{ + "No deployments": {0, 0}, + "3 deployments expected, 1 healthy": {3, 1}, + "3 deployments expected, 3 healthy": {3, 3}, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + buf := new(bytes.Buffer) + c := getInitializedCommand(t, buf) + c.kubernetes = fake.NewSimpleClientset() + + // Deploy deployments if needed. if tc.desired != 0 { - require.Contains(t, actual, fmt.Sprintf("Consul servers healthy %d/%d", tc.healthy, tc.desired)) + err := createDeployments("consul-deployment", namespace, int32(tc.desired), int32(tc.healthy), c.kubernetes) + require.NoError(t, err) + } + + // Verify that the correct deployments statuses are seen. + tbl, err := c.getConsulDeploymentsTable(namespace) + require.NoError(t, err) + + if tc.desired > 0 { + require.NotNil(t, tbl) + expectedHeaders := tableHeaderForConsulComponents + assert.Equal(t, expectedHeaders, tbl.Headers) + + require.Len(t, tbl.Rows, 1) + serverRow := tbl.Rows[0] + + require.Equal(t, "consul-deployment", serverRow[0].Value) + require.Equal(t, fmt.Sprintf("%d/%d", tc.healthy, tc.desired), serverRow[1].Value) + if tc.desired != tc.healthy { + require.Equal(t, terminal.Red, serverRow[1].Color) + } + } else { + require.Nil(t, tbl) } buf.Reset() }) @@ -77,14 +186,29 @@ func TestStatus(t *testing.T) { helmActionsRunner *helm.MockActionRunner expectedReturnCode int }{ - "status with servers returns success": { + "status with checkConsulComponentsStatus returns success": { input: []string{}, messages: []string{ fmt.Sprintf("\n==> Consul Status Summary\nName\tNamespace\tStatus\tChart Version\tAppVersion\tRevision\tLast Updated \n \t \tREADY \t1.0.0 \t \t0 \t%s\t\n", notImeStr), - "\n==> Config:\n {}\n \nConsul servers healthy 3/3\n", + "\n==> Config:\n {}\n", + "\n==> Consul Clients status:", + "\nconsul-client-test1\t3/3", + "\n==> Consul Servers status:", + "\nconsul-server-test1\t3/3", + "\n==> Consul Deployments status:", + "\nconsul-deployment-test1\t3/3", }, preProcessingFunc: func(k8s kubernetes.Interface) error { - return createServers("consul-server-test1", "consul", 3, 3, k8s) + if err := createServers("consul-server-test1", "consul", 3, 3, k8s); err != nil { + return err + } + if err := createClients("consul-client-test1", "consul", 3, 3, k8s); err != nil { + return err + } + if err := createDeployments("consul-deployment-test1", "consul", 3, 3, k8s); err != nil { + return err + } + return nil }, helmActionsRunner: &helm.MockActionRunner{ @@ -107,10 +231,25 @@ func TestStatus(t *testing.T) { messages: []string{ fmt.Sprintf("\n==> Consul Status Summary\nName\tNamespace\tStatus\tChart Version\tAppVersion\tRevision\tLast Updated \n \t \tREADY \t1.0.0 \t \t0 \t%s\t\n", notImeStr), "\n==> Config:\n {}\n \n", - "\n==> Status Of Helm Hooks:\npre-install-hook pre-install: Succeeded\npre-upgrade-hook pre-upgrade: Succeeded\nConsul servers healthy 3/3\n", + "\n==> Status Of Helm Hooks:\npre-install-hook pre-install: Succeeded\npre-upgrade-hook pre-upgrade: Succeeded", + "\n==> Consul Clients status:", + "\nconsul-client-test1\t3/3", + "\n==> Consul Servers status:", + "\nconsul-server-test1\t3/3", + "\n==> Consul Deployments status:", + "\nconsul-deployment-test1\t3/3", }, preProcessingFunc: func(k8s kubernetes.Interface) error { - return createServers("consul-server-test1", "consul", 3, 3, k8s) + if err := createServers("consul-server-test1", "consul", 3, 3, k8s); err != nil { + return err + } + if err := createClients("consul-client-test1", "consul", 3, 3, k8s); err != nil { + return err + } + if err := createDeployments("consul-deployment-test1", "consul", 3, 3, k8s); err != nil { + return err + } + return nil }, helmActionsRunner: &helm.MockActionRunner{ @@ -284,3 +423,36 @@ func createServers(name, namespace string, replicas, readyReplicas int32, k8s ku _, err := k8s.AppsV1().StatefulSets(namespace).Create(context.Background(), &servers, metav1.CreateOptions{}) return err } +func createClients(name, namespace string, replicas, readyReplicas int32, k8s kubernetes.Interface) error { + clients := appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{"app": "consul", "chart": "consul-helm", "component": "client"}, + }, + Status: appsv1.DaemonSetStatus{ + DesiredNumberScheduled: replicas, + NumberReady: readyReplicas, + }, + } + _, err := k8s.AppsV1().DaemonSets(namespace).Create(context.Background(), &clients, metav1.CreateOptions{}) + return err +} +func createDeployments(name, namespace string, replicas, readyReplicas int32, k8s kubernetes.Interface) error { + deployments := appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{"app": "consul", "chart": "consul-helm", "component": "deployment"}, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + }, + Status: appsv1.DeploymentStatus{ + Replicas: replicas, + ReadyReplicas: readyReplicas, + }, + } + _, err := k8s.AppsV1().Deployments(namespace).Create(context.Background(), &deployments, metav1.CreateOptions{}) + return err +}