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

Improve k8s status #563

Merged
merged 19 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/parts/
/stage/
/prime/
.vscode/


**.snap
Expand Down
159 changes: 89 additions & 70 deletions src/k8s/api/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ package apiv1
import (
"fmt"
"strings"

"gopkg.in/yaml.v2"
"time"
)

type ClusterRole string
Expand Down Expand Up @@ -42,6 +41,28 @@ type NodeStatus struct {
DatastoreRole DatastoreRole `json:"datastore-role,omitempty"`
}

// FeatureStatus encapsulates the deployment status of a feature.
type FeatureStatus struct {
// Enabled shows whether or not the deployment of manifests for a status was successful.
Enabled bool
// Message contains information about the status of a feature. It is only supposed to be human readable and informative and should not be programmatically parsed.
Message string
// Version shows the version of the deployed feature.
Version string
// UpdatedAt shows when the last update was done.
UpdatedAt time.Time
}

func (f FeatureStatus) GetMessage() string {
if f.Message != "" {
return f.Message
}
if f.Enabled {
return "enabled"
}
return "disabled"
}

type Datastore struct {
Type string `json:"type,omitempty"`
Servers []string `json:"servers,omitempty" yaml:"servers,omitempty"`
Expand All @@ -54,6 +75,14 @@ type ClusterStatus struct {
Members []NodeStatus `json:"members,omitempty"`
Config UserFacingClusterConfig `json:"config,omitempty"`
Datastore Datastore `json:"datastore,omitempty"`

DNS FeatureStatus `json:"dns,omitempty"`
Network FeatureStatus `json:"network,omitempty"`
LoadBalancer FeatureStatus `json:"load-balancer,omitempty"`
Ingress FeatureStatus `json:"ingress,omitempty"`
Gateway FeatureStatus `json:"gateway,omitempty"`
MetricsServer FeatureStatus `json:"metrics-server,omitempty"`
LocalStorage FeatureStatus `json:"local-storage,omitempty"`
}

// HaClusterFormed returns true if the cluster is in high-availability mode (more than two voter nodes).
Expand All @@ -69,95 +98,85 @@ func (c ClusterStatus) HaClusterFormed() bool {

// TICS -COV_GO_SUPPRESSED_ERROR
// we are just formatting the output for the k8s status command, it is ok to ignore failures from result.WriteString()
func (c ClusterStatus) datastoreToString() string {
result := strings.Builder{}

// Datastore
if c.Datastore.Type != "" {
result.WriteString(fmt.Sprintf(" type: %s\n", c.Datastore.Type))
// Datastore URL for external only
if c.Datastore.Type == "external" {
result.WriteString(fmt.Sprintln(" servers:"))
for _, serverURL := range c.Datastore.Servers {
result.WriteString(fmt.Sprintf(" - %s\n", serverURL))
}
return result.String()
}
}

// Datastore roles for dqlite
voters := make([]NodeStatus, 0, len(c.Members))
standBys := make([]NodeStatus, 0, len(c.Members))
spares := make([]NodeStatus, 0, len(c.Members))
for _, node := range c.Members {
switch node.DatastoreRole {
case DatastoreRoleVoter:
voters = append(voters, node)
case DatastoreRoleStandBy:
standBys = append(standBys, node)
case DatastoreRoleSpare:
spares = append(spares, node)
}
}
if len(voters) > 0 {
result.WriteString(" voter-nodes:\n")
for _, voter := range voters {
result.WriteString(fmt.Sprintf(" - %s\n", voter.Address))
}
} else {
result.WriteString(" voter-nodes: none\n")
}
if len(standBys) > 0 {
result.WriteString(" standby-nodes:\n")
for _, standBy := range standBys {
result.WriteString(fmt.Sprintf(" - %s\n", standBy.Address))
}
} else {
result.WriteString(" standby-nodes: none\n")
}
if len(spares) > 0 {
result.WriteString(" spare-nodes:\n")
for _, spare := range spares {
result.WriteString(fmt.Sprintf(" - %s\n", spare.Address))
}
} else {
result.WriteString(" spare-nodes: none\n")
}

return result.String()
}

// TODO: Print k8s version. However, multiple nodes can run different version, so we would need to query all nodes.
func (c ClusterStatus) String() string {
result := strings.Builder{}

// longer than the longest key (eye-balled), make the output left aligned
maxLen := 25

// Status
if c.Ready {
result.WriteString("status: ready")
result.WriteString(fmt.Sprintf("%-*s %s", maxLen, "cluster status:", "ready"))
} else {
result.WriteString("status: not ready")
result.WriteString(fmt.Sprintf("%-*s %s", maxLen, "cluster status:", "not ready"))
}
result.WriteString("\n")

// Control Plane Nodes
result.WriteString(fmt.Sprintf("%-*s ", maxLen, "control plane nodes:"))
addrMap := c.getCPNodeAddrToRoleMap()
nodes := make([]string, len(addrMap))
i := 0
for addr, role := range addrMap {
nodes[i] = fmt.Sprintf("%s (%s)", addr, role)
i++
HomayoonAlimohammadi marked this conversation as resolved.
Show resolved Hide resolved
}
if len(nodes) > 0 {
result.WriteString(strings.Join(nodes, ", "))
} else {
result.WriteString("none")
}
result.WriteString("\n")

// High availability
result.WriteString("high-availability: ")
result.WriteString(fmt.Sprintf("%-*s ", maxLen, "high availability:"))
if c.HaClusterFormed() {
result.WriteString("yes")
} else {
result.WriteString("no")
}

// Datastore
result.WriteString("\n")
result.WriteString("datastore:\n")
result.WriteString(c.datastoreToString())

// Config
if !c.Config.Empty() {
b, _ := yaml.Marshal(c.Config)
result.WriteString(string(b))
// Datastore
// TODO: how to understand if the ds is running or not?
if c.Datastore.Type != "" {
result.WriteString(fmt.Sprintf("%-*s %s\n", maxLen, "datastore:", c.Datastore.Type))
} else {
result.WriteString(fmt.Sprintf("%-*s %s\n", maxLen, "datastore:", "disabled"))
}

// Network
result.WriteString(fmt.Sprintf("%-*s %s\n", maxLen, "network:", c.Network.GetMessage()))

// DNS
result.WriteString(fmt.Sprintf("%-*s %s\n", maxLen, "dns:", c.DNS.GetMessage()))

// Ingress
result.WriteString(fmt.Sprintf("%-*s %s\n", maxLen, "ingress:", c.Ingress.GetMessage()))

// Load Balancer
result.WriteString(fmt.Sprintf("%-*s %s\n", maxLen, "load-balancer:", c.LoadBalancer.GetMessage()))

// Local Storage
result.WriteString(fmt.Sprintf("%-*s %s\n", maxLen, "local-storage:", c.LocalStorage.GetMessage()))

// Gateway
result.WriteString(fmt.Sprintf("%-*s %s\n", maxLen, "gateway", c.Gateway.GetMessage()))

HomayoonAlimohammadi marked this conversation as resolved.
Show resolved Hide resolved
return result.String()
}

// TICS +COV_GO_SUPPRESSED_ERROR

func (c ClusterStatus) getCPNodeAddrToRoleMap() map[string]string {
m := make(map[string]string)
for _, n := range c.Members {
if n.ClusterRole == ClusterRoleControlPlane {
m[n.Address] = string(n.DatastoreRole)
}
}

return m
}
91 changes: 46 additions & 45 deletions src/k8s/api/v1/types_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package apiv1_test

import (
"fmt"
"testing"

apiv1 "github.com/canonical/k8s/api/v1"
"github.com/canonical/k8s/pkg/utils"
. "github.com/onsi/gomega"
)

Expand Down Expand Up @@ -65,56 +65,51 @@ func TestString(t *testing.T) {
clusterStatus: apiv1.ClusterStatus{
Ready: true,
Members: []apiv1.NodeStatus{
{Name: "node1", DatastoreRole: apiv1.DatastoreRoleVoter, Address: "192.168.0.1"},
{Name: "node2", DatastoreRole: apiv1.DatastoreRoleVoter, Address: "192.168.0.2"},
{Name: "node3", DatastoreRole: apiv1.DatastoreRoleVoter, Address: "192.168.0.3"},
{Name: "node1", DatastoreRole: apiv1.DatastoreRoleVoter, Address: "192.168.0.1", ClusterRole: apiv1.ClusterRoleControlPlane},
{Name: "node2", DatastoreRole: apiv1.DatastoreRoleVoter, Address: "192.168.0.2", ClusterRole: apiv1.ClusterRoleControlPlane},
{Name: "node3", DatastoreRole: apiv1.DatastoreRoleStandBy, Address: "192.168.0.3", ClusterRole: apiv1.ClusterRoleControlPlane},
},
Config: apiv1.UserFacingClusterConfig{
Network: apiv1.NetworkConfig{Enabled: utils.Pointer(true)},
DNS: apiv1.DNSConfig{Enabled: utils.Pointer(true)},
},
Datastore: apiv1.Datastore{Type: "k8s-dqlite"},
Datastore: apiv1.Datastore{Type: "k8s-dqlite"},
Network: apiv1.FeatureStatus{Message: "enabled"},
DNS: apiv1.FeatureStatus{Message: "enabled at 192.168.0.10"},
Ingress: apiv1.FeatureStatus{Message: "enabled"},
LoadBalancer: apiv1.FeatureStatus{Message: "enabled, L2 mode"},
LocalStorage: apiv1.FeatureStatus{Message: "enabled at /var/snap/k8s/common/rawfile-storage"},
Gateway: apiv1.FeatureStatus{Message: "enabled"},
},
expectedOutput: `status: ready
high-availability: yes
datastore:
type: k8s-dqlite
voter-nodes:
- 192.168.0.1
- 192.168.0.2
- 192.168.0.3
standby-nodes: none
spare-nodes: none
network:
enabled: true
dns:
enabled: true
expectedOutput: `cluster status: ready
control plane nodes: 192.168.0.1 (voter), 192.168.0.2 (voter), 192.168.0.3 (stand-by)
high availability: no
datastore: k8s-dqlite
network: enabled
dns: enabled at 192.168.0.10
ingress: enabled
load-balancer: enabled, L2 mode
local-storage: enabled at /var/snap/k8s/common/rawfile-storage
gateway enabled
`,
},
{
name: "External Datastore",
clusterStatus: apiv1.ClusterStatus{
Ready: true,
Members: []apiv1.NodeStatus{
{Name: "node1", DatastoreRole: apiv1.DatastoreRoleVoter, Address: "192.168.0.1"},
},
Config: apiv1.UserFacingClusterConfig{
Network: apiv1.NetworkConfig{Enabled: utils.Pointer(true)},
DNS: apiv1.DNSConfig{Enabled: utils.Pointer(true)},
{Name: "node1", DatastoreRole: apiv1.DatastoreRoleVoter, Address: "192.168.0.1", ClusterRole: apiv1.ClusterRoleControlPlane},
},
Datastore: apiv1.Datastore{Type: "external", Servers: []string{"etcd-url1", "etcd-url2"}},
Network: apiv1.FeatureStatus{Message: "enabled"},
DNS: apiv1.FeatureStatus{Message: "enabled at 192.168.0.10"},
},
expectedOutput: `status: ready
high-availability: no
datastore:
type: external
servers:
- etcd-url1
- etcd-url2
network:
enabled: true
dns:
enabled: true
expectedOutput: `cluster status: ready
control plane nodes: 192.168.0.1 (voter)
high availability: no
datastore: external
network: enabled
dns: enabled at 192.168.0.10
ingress: disabled
load-balancer: disabled
local-storage: disabled
gateway disabled
`,
},
{
Expand All @@ -125,19 +120,25 @@ dns:
Config: apiv1.UserFacingClusterConfig{},
Datastore: apiv1.Datastore{},
},
expectedOutput: `status: not ready
high-availability: no
datastore:
voter-nodes: none
standby-nodes: none
spare-nodes: none
expectedOutput: `cluster status: not ready
control plane nodes: none
high availability: no
datastore: disabled
network: disabled
dns: disabled
ingress: disabled
load-balancer: disabled
local-storage: disabled
gateway disabled
`,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
g := NewWithT(t)
fmt.Println("######################## cluster status:")
fmt.Println(tc.clusterStatus.String())
HomayoonAlimohammadi marked this conversation as resolved.
Show resolved Hide resolved
HomayoonAlimohammadi marked this conversation as resolved.
Show resolved Hide resolved
g.Expect(tc.clusterStatus.String()).To(Equal(tc.expectedOutput))
})
}
Expand Down
3 changes: 2 additions & 1 deletion src/k8s/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/onsi/gomega v1.32.0
github.com/pelletier/go-toml v1.9.5
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.9.0
HomayoonAlimohammadi marked this conversation as resolved.
Show resolved Hide resolved
golang.org/x/net v0.23.0
golang.org/x/sys v0.19.0
gopkg.in/yaml.v2 v2.4.0
Expand Down Expand Up @@ -122,6 +123,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/sftp v1.13.6 // indirect
github.com/pkg/xattr v0.4.9 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.19.0 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.51.1 // indirect
Expand All @@ -133,7 +135,6 @@ require (
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
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.2.0 // indirect
Expand Down
Loading
Loading