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:
Otto Bittner 2023-03-24 11:51:18 +01:00
parent 93e55d2f78
commit c8c2953d7b
19 changed files with 707 additions and 835 deletions

View file

@ -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
}

View file

@ -9,6 +9,7 @@ go_library(
"create.go",
"iam.go",
"rollback.go",
"status.go",
"terminate.go",
"upgrade.go",
"validators.go",

View 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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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",

View file

@ -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
View 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)
}

View 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
}

View file

@ -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.

View file

@ -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(