Skip to content

Commit

Permalink
Improve k8s status (#563)
Browse files Browse the repository at this point in the history
* Reworked k8s status to comply with the new proposal
  • Loading branch information
HomayoonAlimohammadi committed Jul 30, 2024
1 parent 6ae7ac9 commit 032a583
Show file tree
Hide file tree
Showing 31 changed files with 1,187 additions and 244 deletions.
4 changes: 4 additions & 0 deletions docs/src/_parts/commands/k8s_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Retrieve the current status of the cluster

### Synopsis

Retrieve the current status of the cluster as well as deployment status of core features.

```
k8s status [flags]
```
Expand Down
131 changes: 61 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) String() 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,94 +98,56 @@ 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{}

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

// Control Plane Nodes
result.WriteString(fmt.Sprintf("%-25s ", "control plane nodes:"))
if len(c.Members) > 0 {
members := make([]string, 0, len(c.Members))
for _, m := range c.Members {
members = append(members, fmt.Sprintf("%s (%s)", m.Address, m.DatastoreRole))
}
result.WriteString(strings.Join(members, ", "))
} else {
result.WriteString("status: not ready")
result.WriteString("none")
}
result.WriteString("\n")

// High availability
result.WriteString("high-availability: ")
result.WriteString(fmt.Sprintf("%-25s ", "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("%-25s %s\n", "datastore:", c.Datastore.Type))
} else {
result.WriteString(fmt.Sprintf("%-25s %s\n", "datastore:", "disabled"))
}

result.WriteString(fmt.Sprintf("%-25s %s\n", "network:", c.Network))
result.WriteString(fmt.Sprintf("%-25s %s\n", "dns:", c.DNS))
result.WriteString(fmt.Sprintf("%-25s %s\n", "ingress:", c.Ingress))
result.WriteString(fmt.Sprintf("%-25s %s\n", "load-balancer:", c.LoadBalancer))
result.WriteString(fmt.Sprintf("%-25s %s\n", "local-storage:", c.LocalStorage))
result.WriteString(fmt.Sprintf("%-25s %s", "gateway", c.Gateway))

return result.String()
}

Expand Down
91 changes: 43 additions & 48 deletions src/k8s/api/v1/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"testing"

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

Expand Down Expand Up @@ -65,57 +64,50 @@ 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`,
},
{
name: "Cluster not ready, HA not formed, no nodes",
Expand All @@ -125,13 +117,16 @@ 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`,
},
}

Expand Down
1 change: 1 addition & 0 deletions src/k8s/cmd/k8s/k8s_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func newStatusCmd(env cmdutil.ExecutionEnvironment) *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: "Retrieve the current status of the cluster",
Long: "Retrieve the current status of the cluster as well as deployment status of core features.",
PreRun: chainPreRunHooks(hookRequireRoot(env), hookInitializeFormatter(env, &opts.outputFormat)),
Run: func(cmd *cobra.Command, args []string) {
if opts.timeout < minTimeout {
Expand Down
23 changes: 23 additions & 0 deletions src/k8s/pkg/k8sd/api/cluster.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package api

import (
"context"
"database/sql"
"fmt"
"net/http"

apiv1 "github.com/canonical/k8s/api/v1"
"github.com/canonical/k8s/pkg/k8sd/api/impl"
"github.com/canonical/k8s/pkg/k8sd/database"
databaseutil "github.com/canonical/k8s/pkg/k8sd/database/util"
"github.com/canonical/k8s/pkg/k8sd/types"
"github.com/canonical/lxd/lxd/response"
"github.com/canonical/microcluster/state"
)
Expand Down Expand Up @@ -36,6 +40,18 @@ func (e *Endpoints) getClusterStatus(s *state.State, r *http.Request) response.R
return response.InternalError(fmt.Errorf("failed to check if cluster has ready nodes: %w", err))
}

var statuses map[string]types.FeatureStatus
if err := s.Database.Transaction(s.Context, func(ctx context.Context, tx *sql.Tx) error {
var err error
statuses, err = database.GetFeatureStatuses(s.Context, tx)
if err != nil {
return fmt.Errorf("failed to get feature statuses: %w", err)
}
return nil
}); err != nil {
return response.InternalError(fmt.Errorf("database transaction failed: %w", err))
}

result := apiv1.GetClusterStatusResponse{
ClusterStatus: apiv1.ClusterStatus{
Ready: ready,
Expand All @@ -45,6 +61,13 @@ func (e *Endpoints) getClusterStatus(s *state.State, r *http.Request) response.R
Type: config.Datastore.GetType(),
Servers: config.Datastore.GetExternalServers(),
},
DNS: statuses["dns"].ToAPI(),
Network: statuses["network"].ToAPI(),
LoadBalancer: statuses["load-balancer"].ToAPI(),
Ingress: statuses["ingress"].ToAPI(),
Gateway: statuses["gateway"].ToAPI(),
MetricsServer: statuses["metrics-server"].ToAPI(),
LocalStorage: statuses["local-storage"].ToAPI(),
},
}

Expand Down
Loading

0 comments on commit 032a583

Please sign in to comment.