Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show server statuses only when they are deployed into the Kubernetes cluster #1603

Merged
merged 11 commits into from Oct 11, 2022
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,8 @@ FEATURES:
IMPROVEMENTS:
* Helm:
* API Gateway: Allow controller to read MeshServices for use as a route backend. [[GH-1574](https://github.com/hashicorp/consul-k8s/pull/1574)]
* CLI:
* `consul-k8s status` command will only show status of clients and servers if they are expected to be present in the Kubernetes cluster. [[GH-1603](https://github.com/hashicorp/consul-k8s/pull/1603)]

## 1.0.0-beta2 (October 7, 2022)
BREAKING CHANGES:
Expand Down
49 changes: 31 additions & 18 deletions cli/cmd/status/status.go
Expand Up @@ -119,11 +119,9 @@ func (c *Command) Run(args []string) int {
return 1
}

if s, err := c.checkConsulServers(namespace); err != nil {
c.UI.Output(err.Error(), terminal.WithErrorStyle())
if err := c.checkConsulAgents(namespace); err != nil {
c.UI.Output("Unable to check Kubernetes cluster for Consul agents: %v", err)
return 1
} else {
c.UI.Output(s, terminal.WithSuccessStyle())
}

return 0
Expand Down Expand Up @@ -216,24 +214,39 @@ func validEvent(events []release.HookEvent) bool {
return false
}

// checkConsulServers uses the Kubernetes list function to report if the consul servers are healthy.
func (c *Command) checkConsulServers(namespace string) (string, error) {
servers, err := c.kubernetes.AppsV1().StatefulSets(namespace).List(c.Ctx,
metav1.ListOptions{LabelSelector: "app=consul,chart=consul-helm,component=server"})
// checkConsulAgents prints the status of Consul servers and clients 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) checkConsulAgents(namespace string) error {
// Check clients (TODO this can be removed when more users are using Agentless Consul - t-eckert 7 Oct 22)
clients, err := c.kubernetes.AppsV1().DaemonSets(namespace).List(c.Ctx, metav1.ListOptions{LabelSelector: "app=consul,chart=consul-helm"})
if err != nil {
return "", err
} else if len(servers.Items) == 0 {
return "", errors.New("no server stateful set found")
} else if len(servers.Items) > 1 {
return "", errors.New("found multiple server stateful sets")
return err
}
if len(clients.Items) != 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor nit. you could make this a shared func for this and below and pass in:

  • agentType string (servers vs clients. also used for string replacement in the output)
  • desiredAgents int
  • readyAgents int

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate this feedback! I did think about doing that to "DRY" out this function. My thinking with the current way it is written is that one day we will nix the clients check and I don't want an extra function floating around. I think it's nice to just delete the specific lines and not have to change anything else. Once those lines are gone, there won't be any more deduping.

What do you think of that? Am I overthinking it?

desiredClients, readyClients := int(clients.Items[0].Status.DesiredNumberScheduled), int(clients.Items[0].Status.NumberReady)
if readyClients < desiredClients {
c.UI.Output("Consul Clients Healthy %d/%d", readyClients, desiredClients, terminal.WithErrorStyle())
} else {
c.UI.Output("Consul Clients Healthy %d/%d", readyClients, desiredClients)
}
}

desiredReplicas := int(*servers.Items[0].Spec.Replicas)
readyReplicas := int(servers.Items[0].Status.ReadyReplicas)
if readyReplicas < desiredReplicas {
return "", fmt.Errorf("%d/%d Consul servers unhealthy", desiredReplicas-readyReplicas, desiredReplicas)
// Check servers if deployed within Kubernetes cluster.
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
}
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)
}
}
return fmt.Sprintf("Consul servers healthy (%d/%d)", readyReplicas, desiredReplicas), nil

return nil
}

// setupKubeClient to use for non Helm SDK calls to the Kubernetes API The Helm SDK will use
Expand Down
145 changes: 76 additions & 69 deletions cli/cmd/status/status_test.go
Expand Up @@ -28,39 +28,51 @@ import (
"k8s.io/client-go/kubernetes/fake"
)

// TestCheckConsulServers creates a fake stateful set and tests the checkConsulServers function.
func TestCheckConsulServers(t *testing.T) {
c := getInitializedCommand(t, nil)
c.kubernetes = fake.NewSimpleClientset()

// First check that no stateful sets causes an error.
_, err := c.checkConsulServers("default")
require.Error(t, err)
require.Contains(t, err.Error(), "no server stateful set found")

// Next create a stateful set with 3 desired replicas and 3 ready replicas.
var replicas int32 = 3
func TestCheckConsulAgents(t *testing.T) {
namespace := "default"
cases := map[string]struct {
desiredClients int
healthyClients int
desiredServers int
healthyServers int
}{
"No clients, no agents": {0, 0, 0, 0},
"3 clients - 1 healthy, no agents": {3, 1, 0, 0},
"3 clients - 3 healthy, no agents": {3, 3, 0, 0},
"3 clients - 1 healthy, 3 agents - 1 healthy": {3, 1, 3, 1},
"3 clients - 3 healthy, 3 agents - 3 healthy": {3, 3, 3, 3},
"No clients, 3 agents - 1 healthy": {0, 0, 3, 1},
"No clients, 3 agents - 3 healthy": {0, 0, 3, 3},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're using confusing terminology here, it should be clients and servers since agents means both :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great feedback. I fixed this when removing clients.

}

createStatefulSet("consul-server-test1", "default", replicas, replicas, c.kubernetes)
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
buf := new(bytes.Buffer)
c := setupCommand(buf)
c.kubernetes = fake.NewSimpleClientset()

// Now we run the checkConsulServers() function and it should succeed.
s, err := c.checkConsulServers("default")
require.NoError(t, err)
require.Equal(t, "Consul servers healthy (3/3)", s)
// Deploy clients
err := createClients("consul-clients", namespace, int32(tc.desiredClients), int32(tc.healthyClients), c.kubernetes)
require.NoError(t, err)

// If you then create another stateful set it should error.
createStatefulSet("consul-server-test2", "default", replicas, replicas, c.kubernetes)
_, err = c.checkConsulServers("default")
require.Error(t, err)
require.Contains(t, err.Error(), "found multiple server stateful sets")
// Deploy servers
err = createServers("consul-servers", namespace, int32(tc.desiredServers), int32(tc.healthyServers), c.kubernetes)
require.NoError(t, err)

// Clear out the client and now run a test where the stateful set isn't ready.
c.kubernetes = fake.NewSimpleClientset()
createStatefulSet("consul-server-test2", "default", replicas, replicas-1, c.kubernetes)
// Verify that the correct clients and server statuses are seen.
err = c.checkConsulAgents(namespace)
require.NoError(t, err)

_, err = c.checkConsulServers("default")
require.Error(t, err)
require.Contains(t, err.Error(), fmt.Sprintf("%d/%d Consul servers unhealthy", 1, replicas))
actual := buf.String()
if tc.desiredClients != 0 {
require.Contains(t, actual, fmt.Sprintf("Consul Clients Healthy %d/%d", tc.healthyClients, tc.desiredClients))
}
if tc.desiredServers != 0 {
require.Contains(t, actual, fmt.Sprintf("Consul Servers Healthy %d/%d", tc.healthyServers, tc.desiredServers))
}
buf.Reset()
})
}
}

// TestStatus creates a fake stateful set and tests the checkConsulServers function.
Expand All @@ -79,11 +91,11 @@ func TestStatus(t *testing.T) {
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 \n ✓ Consul servers healthy (3/3)\n",
"\n==> Config:\n {}\n \nConsul Clients Healthy 3/3\nConsul Servers Healthy 3/3\n",
},
preProcessingFunc: func(k8s kubernetes.Interface) {
createDaemonset("consul-client-test1", "consul", 3, 3, k8s)
createStatefulSet("consul-server-test1", "consul", 3, 3, k8s)
createClients("consul-client-test1", "consul", 3, 3, k8s)
createServers("consul-server-test1", "consul", 3, 3, k8s)
},

helmActionsRunner: &helm.MockActionRunner{
Expand All @@ -101,40 +113,15 @@ func TestStatus(t *testing.T) {
},
expectedReturnCode: 0,
},
"status with no servers returns error": {
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 \n ! no server stateful set found\n",
},
preProcessingFunc: func(k8s kubernetes.Interface) {
createDaemonset("consul-client-test1", "consul", 3, 3, k8s)
},
helmActionsRunner: &helm.MockActionRunner{
GetStatusFunc: func(status *action.Status, name string) (*helmRelease.Release, error) {
return &helmRelease.Release{
Name: "consul", Namespace: "consul",
Info: &helmRelease.Info{LastDeployed: nowTime, Status: "READY"},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Version: "1.0.0",
},
},
Config: make(map[string]interface{})}, nil
},
},
expectedReturnCode: 1,
},
"status with pre-install and pre-upgrade hooks returns success and outputs hook status": {
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 \n",
"\n==> Status Of Helm Hooks:\npre-install-hook pre-install: Succeeded\npre-upgrade-hook pre-upgrade: Succeeded\n ✓ Consul servers healthy (3/3)\n",
"\n==> Status Of Helm Hooks:\npre-install-hook pre-install: Succeeded\npre-upgrade-hook pre-upgrade: Succeeded\nConsul Servers Healthy 3/3\n",
},
preProcessingFunc: func(k8s kubernetes.Interface) {
createDaemonset("consul-client-test1", "consul", 3, 3, k8s)
createStatefulSet("consul-server-test1", "consul", 3, 3, k8s)
createServers("consul-server-test1", "consul", 3, 3, k8s)
},

helmActionsRunner: &helm.MockActionRunner{
Expand Down Expand Up @@ -187,8 +174,8 @@ func TestStatus(t *testing.T) {
"\n==> Consul Status Summary\n ! kaboom!\n",
},
preProcessingFunc: func(k8s kubernetes.Interface) {
createDaemonset("consul-client-test1", "consul", 3, 3, k8s)
createStatefulSet("consul-server-test1", "consul", 3, 3, k8s)
createClients("consul-client-test1", "consul", 3, 3, k8s)
createServers("consul-server-test1", "consul", 3, 3, k8s)
},

helmActionsRunner: &helm.MockActionRunner{
Expand All @@ -204,8 +191,8 @@ func TestStatus(t *testing.T) {
"\n==> Consul Status Summary\n ! couldn't check for installations: kaboom!\n",
},
preProcessingFunc: func(k8s kubernetes.Interface) {
createDaemonset("consul-client-test1", "consul", 3, 3, k8s)
createStatefulSet("consul-server-test1", "consul", 3, 3, k8s)
createClients("consul-client-test1", "consul", 3, 3, k8s)
createServers("consul-server-test1", "consul", 3, 3, k8s)
},

helmActionsRunner: &helm.MockActionRunner{
Expand Down Expand Up @@ -291,8 +278,8 @@ func TestTaskCreateCommand_AutocompleteArgs(t *testing.T) {
assert.Equal(t, complete.PredictNothing, c)
}

func createStatefulSet(name, namespace string, replicas, readyReplicas int32, k8s kubernetes.Interface) {
ss := &appsv1.StatefulSet{
func createServers(name, namespace string, replicas, readyReplicas int32, k8s kubernetes.Interface) error {
servers := appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Expand All @@ -306,12 +293,12 @@ func createStatefulSet(name, namespace string, replicas, readyReplicas int32, k8
ReadyReplicas: readyReplicas,
},
}

k8s.AppsV1().StatefulSets(namespace).Create(context.Background(), ss, metav1.CreateOptions{})
_, err := k8s.AppsV1().StatefulSets(namespace).Create(context.Background(), &servers, metav1.CreateOptions{})
return err
}

func createDaemonset(name, namespace string, replicas, readyReplicas int32, k8s kubernetes.Interface) {
ds := &appsv1.DaemonSet{
func createClients(name, namespace string, replicas, readyReplicas int32, k8s kubernetes.Interface) error {
clients := appsv1.DaemonSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Expand All @@ -322,6 +309,26 @@ func createDaemonset(name, namespace string, replicas, readyReplicas int32, k8s
NumberReady: readyReplicas,
},
}
_, err := k8s.AppsV1().DaemonSets(namespace).Create(context.Background(), &clients, metav1.CreateOptions{})
return err
}

func setupCommand(buf io.Writer) *Command {
// Log at a test level to standard out.
log := hclog.New(&hclog.LoggerOptions{
Name: "test",
Level: hclog.Debug,
Output: os.Stdout,
})

// Setup and initialize the command struct
command := &Command{
BaseCommand: &common.BaseCommand{
Log: log,
UI: terminal.NewUI(context.Background(), buf),
},
}
command.init()

k8s.AppsV1().DaemonSets(namespace).Create(context.Background(), ds, metav1.CreateOptions{})
return command
}
3 changes: 1 addition & 2 deletions cli/common/terminal/basic.go
Expand Up @@ -79,7 +79,7 @@ func (ui *basicUI) Interactive() bool {
return isatty.IsTerminal(os.Stdin.Fd())
}

// Output implements UI.
// Output prints the given message using the formatting options passed in.
func (ui *basicUI) Output(msg string, raw ...interface{}) {
msg, style, w := ui.parse(msg, raw...)

Expand Down Expand Up @@ -115,7 +115,6 @@ func (ui *basicUI) Output(msg string, raw ...interface{}) {
msg = strings.Join(lines, "\n")
}

// Write it
fmt.Fprintln(w, msg)
}

Expand Down
60 changes: 30 additions & 30 deletions cli/common/terminal/ui.go
Expand Up @@ -7,6 +7,36 @@ import (
"github.com/fatih/color"
)

const (
HeaderStyle = "header"
ErrorStyle = "error"
ErrorBoldStyle = "error-bold"
WarningStyle = "warning"
WarningBoldStyle = "warning-bold"
InfoStyle = "info"
LibraryStyle = "library"
SuccessStyle = "success"
SuccessBoldStyle = "success-bold"
DiffUnchangedStyle = "diff-unchanged"
DiffAddedStyle = "diff-added"
DiffRemovedStyle = "diff-removed"
)

var (
colorHeader = color.New(color.Bold)
colorInfo = color.New()
colorError = color.New(color.FgRed)
colorErrorBold = color.New(color.FgRed, color.Bold)
colorLibrary = color.New(color.FgCyan)
colorSuccess = color.New(color.FgGreen)
colorSuccessBold = color.New(color.FgGreen, color.Bold)
colorWarning = color.New(color.FgYellow)
colorWarningBold = color.New(color.FgYellow, color.Bold)
colorDiffUnchanged = color.New()
colorDiffAdded = color.New(color.FgGreen)
colorDiffRemoved = color.New(color.FgRed)
)

// ErrNonInteractive is returned when Input is called on a non-Interactive UI.
var ErrNonInteractive = errors.New("noninteractive UI doesn't support this operation")

Expand Down Expand Up @@ -65,21 +95,6 @@ type Input struct {
Secret bool
}

const (
HeaderStyle = "header"
ErrorStyle = "error"
ErrorBoldStyle = "error-bold"
WarningStyle = "warning"
WarningBoldStyle = "warning-bold"
InfoStyle = "info"
LibraryStyle = "library"
SuccessStyle = "success"
SuccessBoldStyle = "success-bold"
DiffUnchangedStyle = "diff-unchanged"
DiffAddedStyle = "diff-added"
DiffRemovedStyle = "diff-removed"
)

type config struct {
// Writer is where the message will be written to.
Writer io.Writer
Expand Down Expand Up @@ -167,18 +182,3 @@ func WithStyle(style string) Option {
func WithWriter(w io.Writer) Option {
return func(c *config) { c.Writer = w }
}

var (
colorHeader = color.New(color.Bold)
colorInfo = color.New()
colorError = color.New(color.FgRed)
colorErrorBold = color.New(color.FgRed, color.Bold)
colorLibrary = color.New(color.FgCyan)
colorSuccess = color.New(color.FgGreen)
colorSuccessBold = color.New(color.FgGreen, color.Bold)
colorWarning = color.New(color.FgYellow)
colorWarningBold = color.New(color.FgYellow, color.Bold)
colorDiffUnchanged = color.New()
colorDiffAdded = color.New(color.FgGreen)
colorDiffRemoved = color.New(color.FgRed)
)