mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-07-24 07:50:40 -04:00
cli: add status cmd
The new command allows checking the status of an upgrade and which versions are installed. Also remove the unused restclient. And make GetConstellationVersion a function.
This commit is contained in:
parent
93e55d2f78
commit
c8c2953d7b
19 changed files with 707 additions and 835 deletions
|
@ -59,6 +59,7 @@ func NewRootCmd() *cobra.Command {
|
|||
rootCmd.AddCommand(cmd.NewTerminateCmd())
|
||||
rootCmd.AddCommand(cmd.NewVersionCmd())
|
||||
rootCmd.AddCommand(cmd.NewIAMCmd())
|
||||
rootCmd.AddCommand(cmd.NewStatusCmd())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ go_library(
|
|||
"create.go",
|
||||
"iam.go",
|
||||
"rollback.go",
|
||||
"status.go",
|
||||
"terminate.go",
|
||||
"upgrade.go",
|
||||
"validators.go",
|
||||
|
|
92
cli/internal/cloudcmd/status.go
Normal file
92
cli/internal/cloudcmd/status.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cloudcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
updatev1alpha1 "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api/v1alpha1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// TargetVersions bundles version information about the target versions of a cluster.
|
||||
type TargetVersions struct {
|
||||
// image version
|
||||
image string
|
||||
// CSP specific path to the image
|
||||
imageReference string
|
||||
// kubernetes version
|
||||
kubernetes string
|
||||
}
|
||||
|
||||
// NewTargetVersions returns the target versions for the cluster.
|
||||
func NewTargetVersions(nodeVersion updatev1alpha1.NodeVersion) (TargetVersions, error) {
|
||||
return TargetVersions{
|
||||
image: nodeVersion.Spec.ImageVersion,
|
||||
imageReference: nodeVersion.Spec.ImageReference,
|
||||
kubernetes: nodeVersion.Spec.KubernetesClusterVersion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Image return the image version.
|
||||
func (c *TargetVersions) Image() string {
|
||||
return c.image
|
||||
}
|
||||
|
||||
// ImagePath return the image path.
|
||||
func (c *TargetVersions) ImagePath() string {
|
||||
return c.imageReference
|
||||
}
|
||||
|
||||
// Kubernetes return the Kubernetes version.
|
||||
func (c *TargetVersions) Kubernetes() string {
|
||||
return c.kubernetes
|
||||
}
|
||||
|
||||
// ClusterStatus returns a map from node name to NodeStatus.
|
||||
func ClusterStatus(ctx context.Context, kubeclient kubeClient) (map[string]NodeStatus, error) {
|
||||
nodes, err := kubeclient.GetNodes(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting nodes: %w", err)
|
||||
}
|
||||
|
||||
clusterStatus := map[string]NodeStatus{}
|
||||
for _, node := range nodes {
|
||||
clusterStatus[node.ObjectMeta.Name] = NewNodeStatus(node)
|
||||
}
|
||||
|
||||
return clusterStatus, nil
|
||||
}
|
||||
|
||||
// NodeStatus bundles status information about a node.
|
||||
type NodeStatus struct {
|
||||
kubeletVersion string
|
||||
imageVersion string
|
||||
}
|
||||
|
||||
// NewNodeStatus returns a new NodeStatus.
|
||||
func NewNodeStatus(node corev1.Node) NodeStatus {
|
||||
return NodeStatus{
|
||||
kubeletVersion: node.Status.NodeInfo.KubeletVersion,
|
||||
imageVersion: node.ObjectMeta.Annotations["constellation.edgeless.systems/node-image"],
|
||||
}
|
||||
}
|
||||
|
||||
// KubeletVersion returns the kubelet version of the node.
|
||||
func (n *NodeStatus) KubeletVersion() string {
|
||||
return n.kubeletVersion
|
||||
}
|
||||
|
||||
// ImageVersion returns the node image of the node.
|
||||
func (n *NodeStatus) ImageVersion() string {
|
||||
return n.imageVersion
|
||||
}
|
||||
|
||||
type kubeClient interface {
|
||||
GetNodes(ctx context.Context) ([]corev1.Node, error)
|
||||
}
|
|
@ -40,10 +40,24 @@ import (
|
|||
// ErrInProgress signals that an upgrade is in progress inside the cluster.
|
||||
var ErrInProgress = errors.New("upgrade in progress")
|
||||
|
||||
// GetConstellationVersion queries the constellation-version object for a given field.
|
||||
func GetConstellationVersion(ctx context.Context, client DynamicInterface) (updatev1alpha1.NodeVersion, error) {
|
||||
raw, err := client.GetCurrent(ctx, "constellation-version")
|
||||
if err != nil {
|
||||
return updatev1alpha1.NodeVersion{}, err
|
||||
}
|
||||
var nodeVersion updatev1alpha1.NodeVersion
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.UnstructuredContent(), &nodeVersion); err != nil {
|
||||
return updatev1alpha1.NodeVersion{}, fmt.Errorf("converting unstructured to NodeVersion: %w", err)
|
||||
}
|
||||
|
||||
return nodeVersion, nil
|
||||
}
|
||||
|
||||
// Upgrader handles upgrading the cluster's components using the CLI.
|
||||
type Upgrader struct {
|
||||
stableInterface stableInterface
|
||||
dynamicInterface dynamicInterface
|
||||
dynamicInterface DynamicInterface
|
||||
helmClient helmInterface
|
||||
imageFetcher imageFetcher
|
||||
outWriter io.Writer
|
||||
|
@ -75,7 +89,7 @@ func NewUpgrader(outWriter io.Writer, log debugLog) (*Upgrader, error) {
|
|||
|
||||
return &Upgrader{
|
||||
stableInterface: &stableClient{client: kubeClient},
|
||||
dynamicInterface: &dynamicClient{client: unstructuredClient},
|
||||
dynamicInterface: &NodeVersionClient{client: unstructuredClient},
|
||||
helmClient: helmClient,
|
||||
imageFetcher: image.New(),
|
||||
outWriter: outWriter,
|
||||
|
@ -168,7 +182,7 @@ func (u *Upgrader) KubernetesVersion() (string, error) {
|
|||
|
||||
// CurrentImage returns the currently used image version of the cluster.
|
||||
func (u *Upgrader) CurrentImage(ctx context.Context) (string, error) {
|
||||
nodeVersion, err := u.getConstellationVersion(ctx)
|
||||
nodeVersion, err := GetConstellationVersion(ctx, u.dynamicInterface)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting constellation-version: %w", err)
|
||||
}
|
||||
|
@ -177,7 +191,7 @@ func (u *Upgrader) CurrentImage(ctx context.Context) (string, error) {
|
|||
|
||||
// CurrentKubernetesVersion returns the currently used Kubernetes version.
|
||||
func (u *Upgrader) CurrentKubernetesVersion(ctx context.Context) (string, error) {
|
||||
nodeVersion, err := u.getConstellationVersion(ctx)
|
||||
nodeVersion, err := GetConstellationVersion(ctx, u.dynamicInterface)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting constellation-version: %w", err)
|
||||
}
|
||||
|
@ -248,7 +262,7 @@ func (u *Upgrader) applyNodeVersion(ctx context.Context, nodeVersion updatev1alp
|
|||
}
|
||||
u.log.Debugf("Triggering NodeVersion upgrade now")
|
||||
// Send the updated NodeVersion resource
|
||||
updated, err := u.dynamicInterface.update(ctx, &unstructured.Unstructured{Object: raw})
|
||||
updated, err := u.dynamicInterface.Update(ctx, &unstructured.Unstructured{Object: raw})
|
||||
if err != nil {
|
||||
return updatev1alpha1.NodeVersion{}, fmt.Errorf("updating NodeVersion: %w", err)
|
||||
}
|
||||
|
@ -262,7 +276,7 @@ func (u *Upgrader) applyNodeVersion(ctx context.Context, nodeVersion updatev1alp
|
|||
}
|
||||
|
||||
func (u *Upgrader) checkClusterStatus(ctx context.Context) (updatev1alpha1.NodeVersion, error) {
|
||||
nodeVersion, err := u.getConstellationVersion(ctx)
|
||||
nodeVersion, err := GetConstellationVersion(ctx, u.dynamicInterface)
|
||||
if err != nil {
|
||||
return updatev1alpha1.NodeVersion{}, fmt.Errorf("retrieving current image: %w", err)
|
||||
}
|
||||
|
@ -306,18 +320,38 @@ func (u *Upgrader) updateK8s(nodeVersion *updatev1alpha1.NodeVersion, newCluster
|
|||
return &configMap, nil
|
||||
}
|
||||
|
||||
// getFromConstellationVersion queries the constellation-version object for a given field.
|
||||
func (u *Upgrader) getConstellationVersion(ctx context.Context) (updatev1alpha1.NodeVersion, error) {
|
||||
raw, err := u.dynamicInterface.getCurrent(ctx, "constellation-version")
|
||||
if err != nil {
|
||||
return updatev1alpha1.NodeVersion{}, err
|
||||
}
|
||||
var nodeVersion updatev1alpha1.NodeVersion
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.UnstructuredContent(), &nodeVersion); err != nil {
|
||||
return updatev1alpha1.NodeVersion{}, fmt.Errorf("converting unstructured to NodeVersion: %w", err)
|
||||
}
|
||||
// NodeVersionClient implements the DynamicInterface interface to interact with NodeVersion objects.
|
||||
type NodeVersionClient struct {
|
||||
client dynamic.Interface
|
||||
}
|
||||
|
||||
return nodeVersion, nil
|
||||
// NewNodeVersionClient returns a new NodeVersionClient.
|
||||
func NewNodeVersionClient(client dynamic.Interface) *NodeVersionClient {
|
||||
return &NodeVersionClient{client: client}
|
||||
}
|
||||
|
||||
// GetCurrent returns the current NodeVersion object.
|
||||
func (u *NodeVersionClient) GetCurrent(ctx context.Context, name string) (*unstructured.Unstructured, error) {
|
||||
return u.client.Resource(schema.GroupVersionResource{
|
||||
Group: "update.edgeless.systems",
|
||||
Version: "v1alpha1",
|
||||
Resource: "nodeversions",
|
||||
}).Get(ctx, name, metav1.GetOptions{})
|
||||
}
|
||||
|
||||
// Update updates the NodeVersion object.
|
||||
func (u *NodeVersionClient) Update(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||
return u.client.Resource(schema.GroupVersionResource{
|
||||
Group: "update.edgeless.systems",
|
||||
Version: "v1alpha1",
|
||||
Resource: "nodeversions",
|
||||
}).Update(ctx, obj, metav1.UpdateOptions{})
|
||||
}
|
||||
|
||||
// DynamicInterface is a general interface to query custom resources.
|
||||
type DynamicInterface interface {
|
||||
GetCurrent(ctx context.Context, name string) (*unstructured.Unstructured, error)
|
||||
Update(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error)
|
||||
}
|
||||
|
||||
// upgradeInProgress checks if an upgrade is in progress.
|
||||
|
@ -338,11 +372,6 @@ func upgradeInProgress(nodeVersion updatev1alpha1.NodeVersion) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
type dynamicInterface interface {
|
||||
getCurrent(ctx context.Context, name string) (*unstructured.Unstructured, error)
|
||||
update(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error)
|
||||
}
|
||||
|
||||
type stableInterface interface {
|
||||
getCurrentConfigMap(ctx context.Context, name string) (*corev1.ConfigMap, error)
|
||||
updateConfigMap(ctx context.Context, configMap *corev1.ConfigMap) (*corev1.ConfigMap, error)
|
||||
|
@ -350,28 +379,6 @@ type stableInterface interface {
|
|||
kubernetesVersion() (string, error)
|
||||
}
|
||||
|
||||
type dynamicClient struct {
|
||||
client dynamic.Interface
|
||||
}
|
||||
|
||||
// getCurrent returns the current image definition.
|
||||
func (u *dynamicClient) getCurrent(ctx context.Context, name string) (*unstructured.Unstructured, error) {
|
||||
return u.client.Resource(schema.GroupVersionResource{
|
||||
Group: "update.edgeless.systems",
|
||||
Version: "v1alpha1",
|
||||
Resource: "nodeversions",
|
||||
}).Get(ctx, name, metav1.GetOptions{})
|
||||
}
|
||||
|
||||
// update updates the image definition.
|
||||
func (u *dynamicClient) update(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||
return u.client.Resource(schema.GroupVersionResource{
|
||||
Group: "update.edgeless.systems",
|
||||
Version: "v1alpha1",
|
||||
Resource: "nodeversions",
|
||||
}).Update(ctx, obj, metav1.UpdateOptions{})
|
||||
}
|
||||
|
||||
type stableClient struct {
|
||||
client kubernetes.Interface
|
||||
}
|
||||
|
|
|
@ -426,11 +426,11 @@ type stubDynamicClient struct {
|
|||
updateErr error
|
||||
}
|
||||
|
||||
func (u *stubDynamicClient) getCurrent(_ context.Context, _ string) (*unstructured.Unstructured, error) {
|
||||
func (u *stubDynamicClient) GetCurrent(_ context.Context, _ string) (*unstructured.Unstructured, error) {
|
||||
return u.object, u.getErr
|
||||
}
|
||||
|
||||
func (u *stubDynamicClient) update(_ context.Context, updatedObject *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||
func (u *stubDynamicClient) Update(_ context.Context, updatedObject *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||
u.updatedObject = updatedObject
|
||||
return u.updatedObject, u.updateErr
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ go_library(
|
|||
"miniup.go",
|
||||
"recover.go",
|
||||
"spinner.go",
|
||||
"status.go",
|
||||
"terminate.go",
|
||||
"upgrade.go",
|
||||
"upgradeapply.go",
|
||||
|
@ -67,6 +68,7 @@ go_library(
|
|||
"//internal/versions",
|
||||
"//internal/versionsapi",
|
||||
"//internal/versionsapi/fetcher",
|
||||
"//operators/constellation-node-operator/api/v1alpha1",
|
||||
"//verify/verifyproto",
|
||||
"@com_github_mattn_go_isatty//:go-isatty",
|
||||
"@com_github_siderolabs_talos_pkg_machinery//config/encoder",
|
||||
|
@ -74,6 +76,7 @@ go_library(
|
|||
"@com_github_spf13_cobra//:cobra",
|
||||
"@io_k8s_api//core/v1:core",
|
||||
"@io_k8s_apimachinery//pkg/runtime",
|
||||
"@io_k8s_client_go//dynamic",
|
||||
"@io_k8s_client_go//tools/clientcmd",
|
||||
"@io_k8s_client_go//tools/clientcmd/api/latest",
|
||||
"@io_k8s_sigs_yaml//:yaml",
|
||||
|
@ -97,6 +100,7 @@ go_test(
|
|||
"init_test.go",
|
||||
"recover_test.go",
|
||||
"spinner_test.go",
|
||||
"status_test.go",
|
||||
"terminate_test.go",
|
||||
"upgradeapply_test.go",
|
||||
"upgradecheck_test.go",
|
||||
|
@ -111,6 +115,7 @@ go_test(
|
|||
"//bootstrapper/initproto",
|
||||
"//cli/internal/cloudcmd",
|
||||
"//cli/internal/clusterid",
|
||||
"//cli/internal/helm",
|
||||
"//cli/internal/iamid",
|
||||
"//disk-mapper/recoverproto",
|
||||
"//internal/atls",
|
||||
|
@ -130,6 +135,7 @@ go_test(
|
|||
"//internal/variant",
|
||||
"//internal/versions",
|
||||
"//internal/versionsapi",
|
||||
"//operators/constellation-node-operator/api/v1alpha1",
|
||||
"//verify/verifyproto",
|
||||
"@com_github_spf13_afero//:afero",
|
||||
"@com_github_spf13_cobra//:cobra",
|
||||
|
@ -137,6 +143,9 @@ go_test(
|
|||
"@com_github_stretchr_testify//require",
|
||||
"@in_gopkg_yaml_v3//:yaml_v3",
|
||||
"@io_k8s_api//core/v1:core",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1:meta",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1/unstructured",
|
||||
"@io_k8s_apimachinery//pkg/runtime",
|
||||
"@org_golang_google_grpc//:go_default_library",
|
||||
"@org_golang_google_grpc//codes",
|
||||
"@org_golang_google_grpc//status",
|
||||
|
|
|
@ -33,7 +33,7 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
grpcstatus "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func TestRecoverCmdArgumentValidation(t *testing.T) {
|
||||
|
@ -63,8 +63,8 @@ func TestRecoverCmdArgumentValidation(t *testing.T) {
|
|||
|
||||
func TestRecover(t *testing.T) {
|
||||
someErr := errors.New("error")
|
||||
unavailableErr := status.Error(codes.Unavailable, "unavailable")
|
||||
lbErr := status.Error(codes.Unavailable, `connection error: desc = "transport: authentication handshake failed: read tcp`)
|
||||
unavailableErr := grpcstatus.Error(codes.Unavailable, "unavailable")
|
||||
lbErr := grpcstatus.Error(codes.Unavailable, `connection error: desc = "transport: authentication handshake failed: read tcp`)
|
||||
|
||||
testCases := map[string]struct {
|
||||
doer *stubDoer
|
||||
|
@ -336,7 +336,7 @@ func (d *stubDoer) Do(context.Context) error {
|
|||
if len(d.returns) > 1 {
|
||||
d.returns = d.returns[1:]
|
||||
} else {
|
||||
d.returns = []error{status.Error(codes.Unavailable, "unavailable")}
|
||||
d.returns = []error{grpcstatus.Error(codes.Unavailable, "unavailable")}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
175
cli/internal/cmd/status.go
Normal file
175
cli/internal/cmd/status.go
Normal file
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/helm"
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
"github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl"
|
||||
"github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api/v1alpha1"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
// NewStatusCmd returns a new cobra.Command for the statuus command.
|
||||
func NewStatusCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "show status of a Constellation cluster",
|
||||
Long: "Show status of a constellation cluster.\n\n" +
|
||||
"Shows microservice, image and Kubernetes versions installed in the cluster. Also show status of current version upgrades.",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: runStatus,
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// runStatus runs the terminate command.
|
||||
func runStatus(cmd *cobra.Command, args []string) error {
|
||||
log, err := newCLILogger(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating logger: %w", err)
|
||||
}
|
||||
defer log.Sync()
|
||||
|
||||
kubeClient := kubectl.New()
|
||||
|
||||
fileHandler := file.NewHandler(afero.NewOsFs())
|
||||
kubeConfig, err := fileHandler.Read(constants.AdminConfFilename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading admin.conf: %w", err)
|
||||
}
|
||||
|
||||
// need kubectl client to fetch nodes.
|
||||
if err := kubeClient.Initialize(kubeConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
restConfig, err := clientcmd.RESTConfigFromKubeConfig(kubeConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating k8s client config from kubeconfig: %w", err)
|
||||
}
|
||||
// need unstructed client to fetch NodeVersion CRD.
|
||||
unstructuredClient, err := dynamic.NewForConfig(restConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up custom resource client: %w", err)
|
||||
}
|
||||
|
||||
// need helm client to fetch service versions.
|
||||
helmClient, err := helm.NewClient(kubectl.New(), constants.AdminConfFilename, constants.HelmNamespace, log)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up helm client: %w", err)
|
||||
}
|
||||
|
||||
output, err := status(cmd.Context(), kubeClient, helmClient, cloudcmd.NewNodeVersionClient(unstructuredClient))
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting status: %w", err)
|
||||
}
|
||||
|
||||
cmd.Print(output)
|
||||
return nil
|
||||
}
|
||||
|
||||
// status queries the cluster for the relevant status information and returns the output string.
|
||||
func status(ctx context.Context, kubeClient kubeClient, helmClient helmClient, dynamicInterface cloudcmd.DynamicInterface) (string, error) {
|
||||
nodeVersion, err := cloudcmd.GetConstellationVersion(ctx, dynamicInterface)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting constellation version: %w", err)
|
||||
}
|
||||
if len(nodeVersion.Status.Conditions) != 1 {
|
||||
return "", fmt.Errorf("expected exactly one condition, got %d", len(nodeVersion.Status.Conditions))
|
||||
}
|
||||
|
||||
targetVersions, err := cloudcmd.NewTargetVersions(nodeVersion)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting configured versions: %w", err)
|
||||
}
|
||||
|
||||
serviceVersions, err := helmClient.Versions()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting service versions: %w", err)
|
||||
}
|
||||
|
||||
status, err := cloudcmd.ClusterStatus(ctx, kubeClient)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting cluster status: %w", err)
|
||||
}
|
||||
|
||||
return statusOutput(targetVersions, serviceVersions, status, nodeVersion), nil
|
||||
}
|
||||
|
||||
// statusOutput creates the status cmd output string by formatting the received information.
|
||||
func statusOutput(targetVersions cloudcmd.TargetVersions, serviceVersions helm.ServiceVersions, status map[string]cloudcmd.NodeStatus, nodeVersion v1alpha1.NodeVersion) string {
|
||||
builder := strings.Builder{}
|
||||
|
||||
builder.WriteString(targetVersionsString(targetVersions))
|
||||
builder.WriteString(serviceVersionsString(serviceVersions))
|
||||
builder.WriteString(fmt.Sprintf("Cluster status: %s\n", nodeVersion.Status.Conditions[0].Message))
|
||||
builder.WriteString(nodeStatusString(status, targetVersions))
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// nodeStatusString creates the node status part of the output string.
|
||||
func nodeStatusString(status map[string]cloudcmd.NodeStatus, targetVersions cloudcmd.TargetVersions) string {
|
||||
var upToDateImages int
|
||||
var upToDateK8s int
|
||||
for _, node := range status {
|
||||
if node.KubeletVersion() == targetVersions.Kubernetes() {
|
||||
upToDateK8s++
|
||||
}
|
||||
if node.ImageVersion() == targetVersions.ImagePath() {
|
||||
upToDateImages++
|
||||
}
|
||||
}
|
||||
|
||||
builder := strings.Builder{}
|
||||
if upToDateImages != len(status) || upToDateK8s != len(status) {
|
||||
builder.WriteString(fmt.Sprintf("\tImage: %d/%d\n", upToDateImages, len(status)))
|
||||
builder.WriteString(fmt.Sprintf("\tKubernetes: %d/%d\n", upToDateK8s, len(status)))
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// serviceVersionsString creates the service versions part of the output string.
|
||||
func serviceVersionsString(versions helm.ServiceVersions) string {
|
||||
builder := strings.Builder{}
|
||||
builder.WriteString("Installed service versions:\n")
|
||||
builder.WriteString(fmt.Sprintf("\tCilium: %s\n", versions.Cilium()))
|
||||
builder.WriteString(fmt.Sprintf("\tcert-manager: %s\n", versions.CertManager()))
|
||||
builder.WriteString(fmt.Sprintf("\tconstellation-operators: %s\n", versions.ConstellationOperators()))
|
||||
builder.WriteString(fmt.Sprintf("\tconstellation-services: %s\n", versions.ConstellationServices()))
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// targetVersionsString creates the target versions part of the output string.
|
||||
func targetVersionsString(target cloudcmd.TargetVersions) string {
|
||||
builder := strings.Builder{}
|
||||
builder.WriteString("Target versions:\n")
|
||||
builder.WriteString(fmt.Sprintf("\tImage: %s\n", target.Image()))
|
||||
builder.WriteString(fmt.Sprintf("\tKubernetes: %s\n", target.Kubernetes()))
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
type kubeClient interface {
|
||||
GetNodes(ctx context.Context) ([]corev1.Node, error)
|
||||
}
|
||||
|
||||
type helmClient interface {
|
||||
Versions() (helm.ServiceVersions, error)
|
||||
}
|
193
cli/internal/cmd/status_test.go
Normal file
193
cli/internal/cmd/status_test.go
Normal file
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/helm"
|
||||
updatev1alpha1 "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api/v1alpha1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
const successOutput = `Target versions:
|
||||
Image: v1.1.0
|
||||
Kubernetes: v1.2.3
|
||||
Installed service versions:
|
||||
Cilium: v1.0.0
|
||||
cert-manager: v1.0.0
|
||||
constellation-operators: v1.1.0
|
||||
constellation-services: v1.1.0
|
||||
Cluster status: Node version of every node is up to date
|
||||
`
|
||||
|
||||
const inProgressOutput = `Target versions:
|
||||
Image: v1.1.0
|
||||
Kubernetes: v1.2.3
|
||||
Installed service versions:
|
||||
Cilium: v1.0.0
|
||||
cert-manager: v1.0.0
|
||||
constellation-operators: v1.1.0
|
||||
constellation-services: v1.1.0
|
||||
Cluster status: Some node versions are out of date
|
||||
Image: 1/2
|
||||
Kubernetes: 1/2
|
||||
`
|
||||
|
||||
// TestStatus checks that the status function produces the correct strings.
|
||||
func TestStatus(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
kubeClient stubKubeClient
|
||||
helmClient stubHelmClient
|
||||
nodeVersion updatev1alpha1.NodeVersion
|
||||
dynamicErr error
|
||||
expectedOutput string
|
||||
wantErr bool
|
||||
}{
|
||||
"success": {
|
||||
kubeClient: stubKubeClient{
|
||||
nodes: []corev1.Node{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node1",
|
||||
Annotations: map[string]string{
|
||||
"constellation.edgeless.systems/node-image": "v1.1.0",
|
||||
},
|
||||
},
|
||||
Status: corev1.NodeStatus{
|
||||
NodeInfo: corev1.NodeSystemInfo{
|
||||
KubeletVersion: "v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
helmClient: stubHelmClient{
|
||||
serviceVersions: helm.NewServiceVersions("v1.0.0", "v1.0.0", "v1.1.0", "v1.1.0"),
|
||||
},
|
||||
nodeVersion: updatev1alpha1.NodeVersion{
|
||||
Spec: updatev1alpha1.NodeVersionSpec{
|
||||
ImageVersion: "v1.1.0",
|
||||
ImageReference: "v1.1.0",
|
||||
KubernetesClusterVersion: "v1.2.3",
|
||||
},
|
||||
Status: updatev1alpha1.NodeVersionStatus{
|
||||
Conditions: []metav1.Condition{
|
||||
{
|
||||
Message: "Node version of every node is up to date",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedOutput: successOutput,
|
||||
},
|
||||
"one of two nodes not upgraded": {
|
||||
kubeClient: stubKubeClient{
|
||||
nodes: []corev1.Node{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "outdated",
|
||||
Annotations: map[string]string{
|
||||
"constellation.edgeless.systems/node-image": "v1.0.0",
|
||||
},
|
||||
},
|
||||
Status: corev1.NodeStatus{
|
||||
NodeInfo: corev1.NodeSystemInfo{
|
||||
KubeletVersion: "v1.2.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "uptodate",
|
||||
Annotations: map[string]string{
|
||||
"constellation.edgeless.systems/node-image": "v1.1.0",
|
||||
},
|
||||
},
|
||||
Status: corev1.NodeStatus{
|
||||
NodeInfo: corev1.NodeSystemInfo{
|
||||
KubeletVersion: "v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
helmClient: stubHelmClient{
|
||||
serviceVersions: helm.NewServiceVersions("v1.0.0", "v1.0.0", "v1.1.0", "v1.1.0"),
|
||||
},
|
||||
nodeVersion: updatev1alpha1.NodeVersion{
|
||||
Spec: updatev1alpha1.NodeVersionSpec{
|
||||
ImageVersion: "v1.1.0",
|
||||
ImageReference: "v1.1.0",
|
||||
KubernetesClusterVersion: "v1.2.3",
|
||||
},
|
||||
Status: updatev1alpha1.NodeVersionStatus{
|
||||
Conditions: []metav1.Condition{
|
||||
{
|
||||
Message: "Some node versions are out of date",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedOutput: inProgressOutput,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
raw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tc.nodeVersion)
|
||||
require.NoError(err)
|
||||
output, err := status(context.Background(), tc.kubeClient, tc.helmClient, &stubDynamicInterface{data: unstructured.Unstructured{Object: raw}, err: tc.dynamicErr})
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.expectedOutput, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stubKubeClient struct {
|
||||
nodes []corev1.Node
|
||||
err error
|
||||
}
|
||||
|
||||
func (s stubKubeClient) GetNodes(_ context.Context) ([]corev1.Node, error) {
|
||||
return s.nodes, s.err
|
||||
}
|
||||
|
||||
type stubHelmClient struct {
|
||||
serviceVersions helm.ServiceVersions
|
||||
err error
|
||||
}
|
||||
|
||||
func (s stubHelmClient) Versions() (helm.ServiceVersions, error) {
|
||||
return s.serviceVersions, s.err
|
||||
}
|
||||
|
||||
type stubDynamicInterface struct {
|
||||
data unstructured.Unstructured
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *stubDynamicInterface) GetCurrent(_ context.Context, _ string) (*unstructured.Unstructured, error) {
|
||||
return &s.data, s.err
|
||||
}
|
||||
|
||||
func (s *stubDynamicInterface) Update(_ context.Context, _ *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||
return &s.data, s.err
|
||||
}
|
|
@ -264,7 +264,7 @@ func (v *versionCollector) currentVersions(ctx context.Context) (serviceVersion
|
|||
return "", "", "", fmt.Errorf("setting up helm client: %w", err)
|
||||
}
|
||||
|
||||
serviceVersion, err = helmClient.Versions()
|
||||
serviceVersions, err := helmClient.Versions()
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("getting service versions: %w", err)
|
||||
}
|
||||
|
@ -279,7 +279,7 @@ func (v *versionCollector) currentVersions(ctx context.Context) (serviceVersion
|
|||
return "", "", "", fmt.Errorf("getting image version: %w", err)
|
||||
}
|
||||
|
||||
return serviceVersion, imageVersion, k8sVersion, nil
|
||||
return serviceVersions.ConstellationServices(), imageVersion, k8sVersion, nil
|
||||
}
|
||||
|
||||
// supportedVersions returns slices of supported versions.
|
||||
|
|
|
@ -39,6 +39,9 @@ const (
|
|||
DenyDestructive = false
|
||||
)
|
||||
|
||||
// ErrConfirmationMissing signals that an action requires user confirmation.
|
||||
var ErrConfirmationMissing = errors.New("action requires user confirmation")
|
||||
|
||||
// Client handles interaction with helm and the cluster.
|
||||
type Client struct {
|
||||
config *action.Configuration
|
||||
|
@ -144,13 +147,30 @@ func (c *Client) Upgrade(ctx context.Context, config *config.Config, timeout tim
|
|||
}
|
||||
|
||||
// Versions queries the cluster for running versions and returns a map of releaseName -> version.
|
||||
func (c *Client) Versions() (string, error) {
|
||||
serviceVersion, err := c.currentVersion(constellationServicesInfo.releaseName)
|
||||
func (c *Client) Versions() (ServiceVersions, error) {
|
||||
ciliumVersion, err := c.currentVersion(ciliumInfo.releaseName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting constellation-services version: %w", err)
|
||||
return ServiceVersions{}, fmt.Errorf("getting %s version: %w", ciliumInfo.releaseName, err)
|
||||
}
|
||||
certManagerVersion, err := c.currentVersion(certManagerInfo.releaseName)
|
||||
if err != nil {
|
||||
return ServiceVersions{}, fmt.Errorf("getting %s version: %w", certManagerInfo.releaseName, err)
|
||||
}
|
||||
operatorsVersion, err := c.currentVersion(constellationOperatorsInfo.releaseName)
|
||||
if err != nil {
|
||||
return ServiceVersions{}, fmt.Errorf("getting %s version: %w", constellationOperatorsInfo.releaseName, err)
|
||||
}
|
||||
servicesVersion, err := c.currentVersion(constellationServicesInfo.releaseName)
|
||||
if err != nil {
|
||||
return ServiceVersions{}, fmt.Errorf("getting %s version: %w", constellationServicesInfo.releaseName, err)
|
||||
}
|
||||
|
||||
return compatibility.EnsurePrefixV(serviceVersion), nil
|
||||
return ServiceVersions{
|
||||
cilium: compatibility.EnsurePrefixV(ciliumVersion),
|
||||
certManager: compatibility.EnsurePrefixV(certManagerVersion),
|
||||
constellationOperators: compatibility.EnsurePrefixV(operatorsVersion),
|
||||
constellationServices: compatibility.EnsurePrefixV(servicesVersion),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// currentVersion returns the version of the currently installed helm release.
|
||||
|
@ -174,8 +194,43 @@ func (c *Client) currentVersion(release string) (string, error) {
|
|||
return rel[0].Chart.Metadata.Version, nil
|
||||
}
|
||||
|
||||
// ErrConfirmationMissing signals that an action requires user confirmation.
|
||||
var ErrConfirmationMissing = errors.New("action requires user confirmation")
|
||||
// ServiceVersions bundles the versions of all services that are part of Constellation.
|
||||
type ServiceVersions struct {
|
||||
cilium string
|
||||
certManager string
|
||||
constellationOperators string
|
||||
constellationServices string
|
||||
}
|
||||
|
||||
// NewServiceVersions returns a new ServiceVersions struct.
|
||||
func NewServiceVersions(cilium, certManager, constellationOperators, constellationServices string) ServiceVersions {
|
||||
return ServiceVersions{
|
||||
cilium: cilium,
|
||||
certManager: certManager,
|
||||
constellationOperators: constellationOperators,
|
||||
constellationServices: constellationServices,
|
||||
}
|
||||
}
|
||||
|
||||
// Cilium returns the version of the Cilium release.
|
||||
func (s ServiceVersions) Cilium() string {
|
||||
return s.cilium
|
||||
}
|
||||
|
||||
// CertManager returns the version of the cert-manager release.
|
||||
func (s ServiceVersions) CertManager() string {
|
||||
return s.certManager
|
||||
}
|
||||
|
||||
// ConstellationOperators returns the version of the constellation-operators release.
|
||||
func (s ServiceVersions) ConstellationOperators() string {
|
||||
return s.constellationOperators
|
||||
}
|
||||
|
||||
// ConstellationServices returns the version of the constellation-services release.
|
||||
func (s ServiceVersions) ConstellationServices() string {
|
||||
return s.constellationServices
|
||||
}
|
||||
|
||||
// TODO: v2.8: remove fileHandler argument.
|
||||
func (c *Client) upgradeRelease(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue